82 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
Bilal Catic
a4573a9a13 Merge branch 'reformat-booking-moved-to-another-day-fee-for-the-hundreth-time' into 'master'
reformat booking change incident fee description

See merge request saburly/psihologija!79
2019-12-16 09:30:20 +00:00
Bilal Catic
40aef5ed30 reformat booking change incident fee description 2019-12-16 10:29:52 +01:00
Bilal Catic
f36b1d6628 Merge branch 'remove-first-5-minutes-from-unscheduled-use' into 'master'
remove first N minutes from unscheduled use (N is time resolution)

See merge request saburly/psihologija!78
2019-12-15 17:15:52 +00:00
Bilal Catic
ba5dc504b2 remove first N minutes from unscheduled use (N is time resolution) 2019-12-15 18:15:23 +01:00
Bilal Catic
528626760b Merge branch 'fix-wrong-unscheduled-incident-detection' into 'master'
fix how unlocked and unscheduled incidents are applied

See merge request saburly/psihologija!77
2019-12-13 19:15:55 +00:00
Bilal Catic
501b8cf629 fix how unlocked and unscheduled incidents are applied 2019-12-13 18:36:39 +01:00
Bilal Catic
a604687c61 Merge branch 'fix-wrong-unlock-entry-detection' into 'master'
Fix wrong unlock entry detection

See merge request saburly/psihologija!76
2019-12-12 09:56:33 +00:00
Bilal Catic
b71d869f49 fix wrong exit entry detection 2019-12-12 09:19:33 +01:00
Bilal Catic
3d7db8b156 fix wrong entrance entry detection after reservation start 2019-12-11 20:47:07 +01:00
Bilal Catic
f0b4a069e8 do not delete fees with selected product 2019-12-09 10:37:58 +01:00
Bilal Catic
b16d2c78da Merge branch 'reformat-fees-text' into 'master'
Reformat fees text

See merge request saburly/psihologija!75
2019-12-09 06:58:32 +00:00
Bilal Catic
052f2ac482 change date of shortening fees to the date of booking 2019-12-09 07:39:46 +01:00
Bilal Catic
b13980a16d change date of shortening fees to the date of booking 2019-12-09 06:55:14 +01:00
Bilal Catic
3b442f9b34 change "moved to another day" fee text explanation 2019-12-09 06:51:30 +01:00
Bilal Catic
2dc787d2ed reformat "moved to another day" fee text, show old booking time 2019-12-09 06:51:03 +01:00
Bilal Catic
9d142901df do not re-create deleted incidents 2019-12-06 06:04:45 +01:00
Bilal Catic
988dd275ef Merge branch 'fix-incidents-for-bookings-with-break-between' into 'master'
Fix incidents for bookings with break between

See merge request saburly/psihologija!74
2019-12-06 04:46:42 +00:00
Bilal Catic
d4f205af1f Fix wrong unlocked incident level on the last day of month 2019-12-06 05:44:18 +01:00
Bilal Catic
fa1fa35986 add custom check for incident between two, not back-to-back bookings 2019-12-06 05:39:01 +01:00
Bilal Catic
18022571aa Merge branch 'fix-back-to-back-issue-2-unlocked-fee-applied' into 'master'
fix how unlocked incident fees are applied

See merge request saburly/psihologija!73
2019-12-06 03:35:32 +00:00
Bilal Catic
8944f5181c fix how unlocked incident fees are applied 2019-12-06 03:16:24 +01:00
Bilal Catic
e462f076d0 Merge branch 'fix-repeated-booking' into 'master'
Fix repeated booking

See merge request saburly/psihologija!72
2019-12-05 08:29:30 +00:00
Bilal Catic
f9694570a5 add debugging logs 2019-12-05 09:27:39 +01:00
Bilal Catic
166e2a0a9f look for all door lock events before if from timestamp is not defined 2019-12-05 09:25:18 +01:00
Bilal Catic
6df1a798ee transform repeated booking to series of regular bookings 2019-12-04 12:42:32 +01:00
Bilal Catic
4367ca4259 bookings are now added from fees array - for repeated bookings 2019-12-03 18:23:20 +01:00
Bilal Catic
0ef109ffce insert debugging logs, comment for now 2019-12-03 18:21:38 +01:00
Bilal Catic
a14ca5aaa7 remove console logging 2019-12-03 18:20:34 +01:00
Bilal Catic
2c24028999 Merge branch 'fix-undetected-unscheduled-use-incidents' into 'master'
synchronize incidents detection and save incidents to the DB

See merge request saburly/psihologija!71
2019-12-02 19:59:44 +00:00
Bilal Catic
c30fb1adf6 synchronize incidents detection and save incidents to the DB 2019-12-02 20:57:46 +01:00
Bilal Catic
fb959491d9 Merge branch 'implement-silent-fees-delete' into 'master'
add silent option to Delete fees request

See merge request saburly/psihologija!70
2019-11-26 12:18:04 +00:00
Bilal Catic
b1aad87601 add silent option to Delete fees request 2019-11-26 13:17:28 +01:00
Bilal Catic
9494fa48c5 Merge branch 'change-unscheduled-use-charge-time-limits' into 'master'
allow first time segment length for unscheduled use to be configurable

See merge request saburly/psihologija!69
2019-11-22 19:23:21 +00:00
Bilal Catic
93d46c231e allow first time segment length for unscheduled use to be configurable 2019-11-22 20:22:44 +01:00
Bilal Catic
1436f0fdab Merge branch 'change-fees-formatting' into 'master'
change fees formatting

See merge request saburly/psihologija!68
2019-11-22 07:59:09 +00:00
Bilal Catic
7c8a5e954e change fees formatting 2019-11-22 08:58:36 +01:00
Bilal Catic
bc4db56e54 Merge branch 'allow-deleting-incident-fees' into 'master'
fix "Delete fee" on Member Incidents Report screen

See merge request saburly/psihologija!67
2019-11-21 14:16:28 +00:00
Bilal Catic
06a40e98e9 fix "Delete fee" on Member Incidents Report screen 2019-11-21 15:13:37 +01:00
Bilal Catic
d4a949aaf0 Merge branch 'allow-deleting-incident-fees' into 'master'
Allow deleting incident fees

See merge request saburly/psihologija!66
2019-11-21 13:36:59 +00:00
Bilal Catic
fa4e574d6e remove obsolete logging 2019-11-21 14:34:50 +01:00
Bilal Catic
445a635300 split fees into specific categories and send delete request 2019-11-21 14:32:52 +01:00
Bilal Catic
d1d67c346a add checkbox option to the incident tables 2019-11-21 09:21:21 +01:00
Bilal Catic
37abb86961 add and implement action to send "delete fees" request 2019-11-21 07:13:30 +01:00
Bilal Catic
8a3db0d481 add deleteFees route and implement logic 2019-11-21 07:12:50 +01:00
Bilal Catic
3d051766b1 fetch only non-deleted unlocked/unscheduled incidents 2019-11-20 15:26:20 +01:00
Bilal Catic
d4f823ff72 add deleted field to the unlocked/unscheduled incidents table and model 2019-11-20 14:53:53 +01:00
Bilal Catic
015c29f992 Merge branch 'add-door-unlock-lock-time-column-in-incidents-table' into 'master'
Add door unlock lock time column in incidents table

See merge request saburly/psihologija!65
2019-11-20 12:50:56 +00:00
Bilal Catic
cd79a47462 set main container width configurable 2019-11-20 13:46:41 +01:00
Bilal Catic
c0c4c1f4a1 add door unlock/lock time column to the incidents table 2019-11-20 13:46:12 +01:00
Bilal Catic
eff96f9e5e fix mapping screen crash when office/resource is unknown 2019-11-20 13:45:54 +01:00
Bilal Catic
fcb71a3f00 Merge branch 'change-fees-formatting' into 'master'
fix fee formatting issues

See merge request saburly/psihologija!64
2019-11-20 11:28:30 +00:00
Bilal Catic
136cad8bbd fix fee formatting issues 2019-11-20 12:27:42 +01:00
Senad Uka
4ea9d37915 Date range for sending fees 2019-10-28 20:42:18 +01:00
Bilal Catic
9ac1f57396 apply weekend rate to the reservation if applicable 2019-10-28 20:02:57 +01:00
Bilal Catic
4946b051c5 Merge branch 'fix-name-compare' into 'master'
compare only part of the name

See merge request saburly/psihologija!63
2019-10-07 13:42:24 +00:00
Bilal Catic
0d9c6ce67e compare only part of the name 2019-10-07 15:41:46 +02:00
Bilal Catic
c1110f3678 Merge branch 'fix-manually-booking-fee-delete' into 'master'
delete reservations removed from ORD

See merge request saburly/psihologija!62
2019-10-07 13:17:26 +00:00
Bilal Catic
8ac69f1497 delete reservations removed from ORD 2019-10-07 15:16:09 +02:00
Bilal Catic
639ba6b8cb fix practice summary report 2019-10-07 09:55:00 +02:00
Bilal Catic
aa8931f91a fix discount date 2019-10-04 19:51:39 +02:00
Bilal Catic
ef932ed36b add discount on first day of the month 2019-10-04 19:41:51 +02:00
32 changed files with 1845 additions and 554 deletions

View File

@@ -0,0 +1,142 @@
import React, { Component } from 'react';
import moment from 'moment';
import { Form, Message, Grid } from 'semantic-ui-react';
import { defaultDateFormat } from '../../constants/constants';
class DateRangePicker extends Component {
constructor(props) {
super(props);
const initialStartDate = props.startDate ? moment(props.startDate, defaultDateFormat) : moment().startOf('month');
let initialEndDate = props.endDate ? moment(props.endDate, defaultDateFormat) : moment();
if (initialStartDate > initialEndDate){
initialEndDate = initialStartDate.add(1, 'day');
}
this.state = {
startDate: initialStartDate,
endDate: initialEndDate,
error: null,
startDateLabel: props.startDateLabel || 'Start date',
endDateLabel: props.endDateLabel || 'End date',
};
}
onStartDateChange(event) {
const { endDate, startDateLabel, endDateLabel } = this.state;
const newStartDate = moment(event.target.value, defaultDateFormat);
if (newStartDate > endDate){
this.setState({
error: `${startDateLabel} cannot be after ${endDateLabel}`
});
return;
}
this.setState({startDate: newStartDate, error: null});
}
onEndDateChange(event) {
const { startDate, startDateLabel, endDateLabel } = this.state;
const newEndDate = moment(event.target.value, defaultDateFormat);
if (newEndDate < startDate){
this.setState({
error: `${startDateLabel} cannot be after ${endDateLabel}`
});
return;
}
this.setState({endDate: newEndDate, error: null});
}
onDismiss() {
this.setState({error: null});
}
onButtonClick() {
const { onDatesUpdate } = this.props;
const { startDate, endDate } = this.state;
if (onDatesUpdate){
onDatesUpdate({
startDate: startDate.format(defaultDateFormat),
endDate: endDate.format(defaultDateFormat),
});
}
}
componentDidMount() {
this.onButtonClick();
}
render() {
const { startDate, endDate, error, startDateLabel, endDateLabel } = this.state;
const { inlineButton } = this.props;
const buttonLabel = this.props.buttonLabel || 'Save';
const startDateValue = startDate.format(defaultDateFormat);
const endDateValue = endDate.format(defaultDateFormat);
const buttonRender = (
<Grid.Column width={inlineButton ? 3 : null}>
{ inlineButton && <label>{'\u00A0'}</label> }
<Form.Button onClick={this.onButtonClick.bind(this)}>{buttonLabel}</Form.Button>
</Grid.Column>
);
return (
<Form>
<Grid stackable columns={inlineButton ? null : 'equal'}>
<Grid.Row>
<Grid.Column width={inlineButton ? 7 : null}>
<label>{startDateLabel}</label>
<Form.Input
fluid
required
type="date"
value={startDateValue}
onChange={this.onStartDateChange.bind(this)}
/>
</Grid.Column>
<Grid.Column width={inlineButton ? 6 : null}>
<label>{endDateLabel}</label>
<Form.Input
fluid
required
type="date"
value={endDateValue}
onChange={this.onEndDateChange.bind(this)}
/>
</Grid.Column>
{
inlineButton && buttonRender
}
</Grid.Row>
{ error &&
<Grid.Row>
<Grid.Column>
<Message color="orange" onDismiss={this.onDismiss.bind(this)}>
<Message.Header>Wrong date</Message.Header>
<p><b>{startDateLabel}</b> has to be before <b>{endDateLabel}</b></p>
</Message>
</Grid.Column>
</Grid.Row>
}
{
!inlineButton &&
<Grid.Row>
{buttonRender}
</Grid.Row>
}
</Grid>
</Form>
);
}
}
export default DateRangePicker;

View File

@@ -1,167 +1,384 @@
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,
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, updateIncidentFees } from "../../../store/actions";
class SingleIncidentsTable extends Component {
state = {
selectedUnlockedIncidentIds: [],
selectedUnscheduledIncidentIds: [],
selectedBookingChangeIncidentIds: [],
changedUnlockedIncidentIds: {},
changedUnscheduledIncidentIds: {},
changedBookingChangeIncidentIds: {},
inputIdToFocus: null,
};
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 <NavLink to={urlValue}>{cellValue}</NavLink>
}else{
return <div style={{ textAlign: columnContentsAlignment, whiteSpace: 'pre' }}>{cellValue}</div>
}
}
});
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 (
<div>
<h4>{title}</h4>
<Loader active={loading} />
{
!loading && incidents &&
<ReactTable
data={incidents}
multiSort={false}
columns={columns}
/>
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: []
});
};
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,
title,
openMemberSummaryOnMemberClick,
hideMemberName,
tableType
} = this.props;
const {
selectedUnlockedIncidentIds,
selectedUnscheduledIncidentIds,
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}`;
});
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;
}
</div>
);
};
export default SingleIncidentsTable;
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];
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;
let clickablePrice = undefined;
let priceAsNumber = undefined;
let priceInputID = undefined;
let priceFontColor = 'black';
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':
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':
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 (clickablePrice){
return <p>$ <input
id={priceInputID}
style={{ color: priceFontColor }}
type={'number'}
onChange={priceChangeHandler}
value={priceAsNumber}
autoFocus={priceInputID === inputIdToFocus}
/>
</p>
}else{
if (openMemberSummaryOnMemberClick && urlValue){
return <NavLink to={urlValue}>{cellValue}</NavLink>
}else{
return <div style={{ textAlign: columnContentsAlignment, whiteSpace: 'pre' }}>{cellValue}</div>
}
}
}
});
}
});
}
return (
<div>
<h4>{title}</h4>
<Loader active={loading} />
{
<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 &&
<SelectTable
data={incidents}
multiSort={false}
columns={columns}
keyField="id"
onSelectChange={this.onSelectChange}
/>
}
</div>
);
}
}
const mapDispatchToProps = (dispatch) => ({
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

@@ -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}
/>
);

View File

@@ -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 (
<SelectTableElement
{...this.props}
ref={r => (this.checkboxTable = r)}
toggleSelection={this.toggleSelection}
selectAll={this.state.selectAll}
selectType="checkbox"
toggleAll={this.toggleAll}
isSelected={this.isSelected}
getTrProps={this.rowFn}
/>
);
}
}
export default SelectTable;

View File

@@ -1,3 +1,5 @@
export const CONTAINER_WIDTH_PERCENTAGE = 90;
export const defaultDateFormat = 'YYYY-MM-DD';
export const doorLockRelatedWithReservationIncidentHeaders = [
@@ -5,6 +7,7 @@ export const doorLockRelatedWithReservationIncidentHeaders = [
'resourceName',
'memberName',
'reservation',
'doorLockTimestamps',
'incidentType',
'feeDescription',
'totalChargeFee'

View File

@@ -4,12 +4,13 @@ import { Container, Message } from 'semantic-ui-react';
import MainMenu from '../../components/MainMenu';
import { mainMenuItems } from '../../constants/menuItems';
import {CONTAINER_WIDTH_PERCENTAGE} from "../../constants/constants";
class Home extends Component {
render () {
return (
<Container>
<Container style={ {width: `${CONTAINER_WIDTH_PERCENTAGE}%`} }>
<MainMenu/>
<h3>SIMA SPACE </h3>
<hr/>

View File

@@ -3,11 +3,12 @@ import { connect } from 'react-redux';
import { Container } from 'semantic-ui-react';
import MainMenu from '../../components/MainMenu';
import MonthSelector from '../../components/MonthSelector';
import DateRangePicker from '../../components/DateRangePicker';
import MemberIncidentsTables from '../../components/MemberIncidentsTables';
import GenerateFeesInORDButton from '../../components/GenerateFeesInORDButton';
import { fetchIncidents } from '../../store/actions';
import { CONTAINER_WIDTH_PERCENTAGE } from '../../constants/constants';
class IncidentsReport extends Component {
state = {dateRange: null};
@@ -33,11 +34,11 @@ class IncidentsReport extends Component {
}
return (
<Container>
<Container style={ {width: `${CONTAINER_WIDTH_PERCENTAGE}%`} }>
<MainMenu/>
<h3>Incidents Report</h3>
<hr/>
<MonthSelector buttonLabel="Show report" onDatesUpdate={this.onDatesUpdate} inlineButton />
<DateRangePicker buttonLabel="Show report" onDatesUpdate={this.onDatesUpdate} inlineButton />
<br/>
<GenerateFeesInORDButton
disabled={loading}
@@ -46,7 +47,7 @@ class IncidentsReport extends Component {
<br/><br/>
<hr/>
<br/>
<MemberIncidentsTables pendingIncidents={loading} incidents={incidents} />
<MemberIncidentsTables pendingIncidents={loading} incidents={incidents} dateRange={dateRange} />
</Container>
);
}

View File

@@ -5,6 +5,7 @@ import { Container, Button, Loader, Input, Message, Grid } from 'semantic-ui-rea
import MainMenu from '../../components/MainMenu';
import { fetchMemberPracticeSummaryReport } from '../../store/actions';
import {CONTAINER_WIDTH_PERCENTAGE} from "../../constants/constants";
class MemberPracticeSummaryReport extends Component {
@@ -54,7 +55,7 @@ class MemberPracticeSummaryReport extends Component {
error = fetchReportError ? fetchReportError : error;
return (
<Container>
<Container style={ {width: `${CONTAINER_WIDTH_PERCENTAGE}%`} }>
<MainMenu/>
<h3>Member Practice Summary Report</h3>
<hr/>

View File

@@ -76,67 +76,71 @@ class SingleMapping extends Component {
};
render() {
const { singleMapping: { officeSlug, resourceSlug } } = this.props;
const { officeId, resourceId, officeOptions, allResourceOptions, pendingMappings } = this.props;
const { showDeletePrompt, newOfficeId, newResourceId } = this.state;
try {
const {singleMapping: {officeSlug, resourceSlug}} = this.props;
const {officeId, resourceId, officeOptions, allResourceOptions, pendingMappings} = this.props;
const {showDeletePrompt, newOfficeId, newResourceId} = this.state;
const selectedOfficeId = newOfficeId ? newOfficeId : officeId;
const selectedResourceId = newResourceId ? newResourceId : resourceId;
const selectedOfficeId = newOfficeId ? newOfficeId : officeId;
const selectedResourceId = newResourceId ? newResourceId : resourceId;
const resourceOptions = allResourceOptions && officeId ? allResourceOptions[selectedOfficeId].roomOptions : [];
const resourceOptions = allResourceOptions && officeId ? allResourceOptions[selectedOfficeId].roomOptions : [];
const enableActions = !pendingMappings;
const enableSave = enableActions && ((newOfficeId !== null) || (newResourceId !== null));
const saveIconColor = enableSave ? 'green' : null;
const enableActions = !pendingMappings;
const enableSave = enableActions && ((newOfficeId !== null) || (newResourceId !== null));
const saveIconColor = enableSave ? 'green' : null;
return (
<Table.Row>
<PromptMessage
onClose={this.onPromptClose}
onActionNo={this.onPromptClose}
onActionYes={this.onPromptYes}
title={'Delete mapping'}
message={'Do you want to delete this mapping ?'}
show={showDeletePrompt}
/>
<Table.Cell>
<Label size={'big'}>{`[${officeSlug}-${resourceSlug}]`}</Label>
</Table.Cell>
<Table.Cell>
<Dropdown
options={officeOptions}
defaultValue={officeId}
onChange={this.onOfficeChange}
return (
<Table.Row>
<PromptMessage
onClose={this.onPromptClose}
onActionNo={this.onPromptClose}
onActionYes={this.onPromptYes}
title={'Delete mapping'}
message={'Do you want to delete this mapping ?'}
show={showDeletePrompt}
/>
</Table.Cell>
<Table.Cell>
<Dropdown
options={resourceOptions}
value={selectedResourceId}
onChange={this.onResourceChange}
/>
</Table.Cell>
<Table.Cell>
<Icon
circular
name={'check'}
size={'large'}
disabled={!enableSave}
link={enableSave || null}
color={saveIconColor}
onClick={this.onMappingUpdate}
/>
<Icon
circular
name={'trash'}
size={'large'}
disabled={!enableActions}
link={enableActions || null}
onClick={this.deleteMapping}
/>
</Table.Cell>
</Table.Row>
);
<Table.Cell>
<Label size={'big'}>{`[${officeSlug}-${resourceSlug}]`}</Label>
</Table.Cell>
<Table.Cell>
<Dropdown
options={officeOptions}
defaultValue={officeId}
onChange={this.onOfficeChange}
/>
</Table.Cell>
<Table.Cell>
<Dropdown
options={resourceOptions}
value={selectedResourceId}
onChange={this.onResourceChange}
/>
</Table.Cell>
<Table.Cell>
<Icon
circular
name={'check'}
size={'large'}
disabled={!enableSave}
link={enableSave || null}
color={saveIconColor}
onClick={this.onMappingUpdate}
/>
<Icon
circular
name={'trash'}
size={'large'}
disabled={!enableActions}
link={enableActions || null}
onClick={this.deleteMapping}
/>
</Table.Cell>
</Table.Row>
);
}catch (e) {
return null;
}
}
}

View File

@@ -5,6 +5,7 @@ import { Container, Message, Loader, Table } from 'semantic-ui-react';
import MainMenu from '../../components/MainMenu';
import { fetchMappings } from '../../store/actions';
import SingleMapping from './components/SingleMapping';
import {CONTAINER_WIDTH_PERCENTAGE} from "../../constants/constants";
class RoomOfficeNameMapping extends Component {
@@ -47,7 +48,7 @@ class RoomOfficeNameMapping extends Component {
});
return (
<Container>
<Container style={ {width: `${CONTAINER_WIDTH_PERCENTAGE}%`} }>
<Loader active={pendingMappings} />
<MainMenu/>
<h3>Room Office Mapping</h3>

View File

@@ -3,13 +3,14 @@ import { connect } from 'react-redux';
import { Container, Grid } from 'semantic-ui-react';
import MainMenu from '../../components/MainMenu';
import MonthSelector from '../../components/MonthSelector';
import DateRangePicker from '../../components/DateRangePicker';
import MemberSelector from './components/MemberSelector';
import MemberSummary from './components/MemberSummary';
import MemberIncidentsTables from '../../components/MemberIncidentsTables';
import GenerateFeesInORDButton from '../../components/GenerateFeesInORDButton';
import { fetchMemberIncidents } from '../../store/actions';
import {CONTAINER_WIDTH_PERCENTAGE} from "../../constants/constants";
class SpecificMemberIncidentsReport extends Component {
constructor(props){
@@ -48,7 +49,7 @@ class SpecificMemberIncidentsReport extends Component {
const addFeesButtonDisabled = !memberId || !dateRange || loading;
return (
<Container>
<Container style={ {width: `${CONTAINER_WIDTH_PERCENTAGE}%`} }>
<MainMenu/>
<h3>Member Incidents Report</h3>
<hr/>
@@ -58,7 +59,7 @@ class SpecificMemberIncidentsReport extends Component {
<MemberSelector onMemberSelect={this.onMemberSelectionUpdate.bind(this)} defaultMemberId={memberId} />
</Grid.Column>
<Grid.Column>
<MonthSelector inlineButton onDatesUpdate={this.onDateRangeUpdate.bind(this)}/>
<DateRangePicker inlineButton onDatesUpdate={this.onDateRangeUpdate.bind(this)}/>
</Grid.Column>
</Grid.Row>
<Grid.Row>
@@ -82,7 +83,13 @@ class SpecificMemberIncidentsReport extends Component {
<Grid.Row/>
<Grid.Row>
<Grid.Column>
<MemberIncidentsTables incidents={memberIncidents} pendingIncidents={loading} hideMemberName/>
<MemberIncidentsTables
incidents={memberIncidents}
pendingIncidents={loading}
hideMemberName
dateRange={dateRange}
memberId={memberId}
/>
</Grid.Column>
</Grid.Row>
</Grid>

View File

@@ -4,10 +4,11 @@ import { Container, Form } from 'semantic-ui-react';
import MainMenu from '../../components/MainMenu';
import FileUpload from './components/FileUpload';
import UploadResults from './components/UploadResults';
import {CONTAINER_WIDTH_PERCENTAGE} from "../../constants/constants";
function UploadDLockData() {
return (
<Container>
<Container style={ {width: `${CONTAINER_WIDTH_PERCENTAGE}%`} }>
<MainMenu/>
<h3>DLock Data</h3>
<hr/>

View File

@@ -88,6 +88,36 @@ 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 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

@@ -14,37 +14,37 @@ const unlockedIncidentLevelsPrices = {
id: 0,
title: 'UNLOCKED_0',
price: parseInt(process.env.UNLOCK_0) || 0,
description: 'First month - warning',
description: 'first month - warning',
},
UNLOCKED_1: {
id: 1,
title: 'UNLOCKED_1',
price: parseInt(process.env.UNLOCK_1) || 10,
description: 'Second month',
description: 'second month',
},
UNLOCKED_2: {
id: 2,
title: 'UNLOCKED_2',
price: parseInt(process.env.UNLOCK_2) || 20,
description: 'Third month',
description: 'third month',
},
UNLOCKED_3: {
id: 3,
title: 'UNLOCKED_3',
price: parseInt(process.env.UNLOCK_3) || 30,
description: 'Fourth month',
description: 'fourth month',
},
UNLOCKED_4: {
id: 4,
title: 'UNLOCKED_4',
price: parseInt(process.env.UNLOCK_4) || 40,
description: 'Fifth month',
description: 'fifth month',
},
UNLOCKED_5: {
id: 5,
title: 'UNLOCKED_5',
price: parseInt(process.env.UNLOCK_5) || 50,
description: 'Sixth month and onward',
description: 'sixth month and onward',
}
};
const csvParserErrors = {
@@ -57,6 +57,8 @@ const csvParserErrors = {
const officeRnDAPIErrors = {
FAILED_TO_FETCH_MEMBERS: 'Failed to fetch members',
FAILED_TO_FETCH_BOOKINGS: 'Failed to fetch booking reservations',
FAILED_TO_CREATE_BOOKINGS: 'Failed to create booking reservations',
FAILED_TO_DELETE_BOOKINGS: 'Failed to delete booking reservations',
FAILED_TO_FETCH_FEES: 'Failed to fetch existing fees from ORD',
FAILED_TO_FETCH_PLANS: 'Failed to fetch plans from ORD',
FAILED_TO_DELETE_FEES: 'Failed to delete fees in ORD',
@@ -95,20 +97,21 @@ const incidentType = {
};
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';
incidentTypeExplanations[incidentType.UNSCHEDULED_INCIDENT_AFTER_RESERVATION] = 'Room used after reservation';
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 after grace period';
incidentTypeExplanations[incidentType.BOOKING_CANCELED_LATE] = 'Reservation cancelled after grace period';
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';
incidentTypeExplanations[incidentType.UNSCHEDULED_INCIDENT_AFTER_RESERVATION] = 'room used after reservation';
incidentTypeExplanations[incidentType.UNSCHEDULED_INCIDENT_STANDALONE] = 'room used without reservation';
incidentTypeExplanations[incidentType.BOOKING_MOVED_TO_ANOTHER_DAY] = 'reservation moved to another day in less than 24 hrs';
incidentTypeExplanations[incidentType.BOOKING_SHORTENED] = 'reservation shortened after grace period';
incidentTypeExplanations[incidentType.BOOKING_CANCELED_LATE] = 'reservation cancelled after grace period';
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;
@@ -135,10 +138,9 @@ const CUSTOM_FEES_PREFIXES = process.env.CUSTOM_FEES_PREFIXES.split(',')
const UNPAID_FEE_STATUS = 'not_paid';
const ALLOW_SENDING_FEES = !!parseInt(process.env.ALLOW_SENDING_FEES_FOR_CURRENT_AND_FUTURE_MONTHS);
const ALLOW_SENDING_FEES = parseInt(process.env.ALLOW_SENDING_FEES_FOR_CURRENT_AND_FUTURE_MONTHS) ? true : false;
const MAX_FEES_TO_DELETE = parseInt(process.env.MAX_FEES_TO_DELETE) || 10;
const FEES_DELETE_DELAY = parseInt(process.env.FEES_DELETE_DELAY) || 0;
const TRIM_DLOCK_NAMES_LENGTH = parseInt(process.env.TRIM_DLOCK_NAMES_LENGTH) || 22;
module.exports = {
VALID_CSV_HEADERS,
@@ -155,6 +157,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,
@@ -165,6 +168,5 @@ module.exports = {
UNPAID_FEE_STATUS,
CUSTOM_FEES_PREFIXES,
ALLOW_SENDING_FEES,
MAX_FEES_TO_DELETE,
FEES_DELETE_DELAY
TRIM_DLOCK_NAMES_LENGTH
};

View File

@@ -2,13 +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,
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');
@@ -175,6 +187,92 @@ 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 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) => {
@@ -227,4 +325,6 @@ module.exports = {
addFees,
checkProcessingStatus,
getPracticeSummaryReport,
deleteFees,
updateFees
};

View File

@@ -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
@@ -41,8 +42,7 @@ CUSTOM_FEES_PREFIXES=Array of prefixes to recognize manually added fees. Comma-s
ALLOW_SENDING_FEES_FOR_CURRENT_AND_FUTURE_MONTHS=0 - false => Sending fees is disabled for current and future months, 1 - true => Sending fees is never disabled
MAX_FEES_TO_DELETE=Max number of fees to delete (from ORD) in one API call
FEES_DELETE_DELAY=Number of miliseconds to wait between two API calls to delete fees (from ORD)
TRIM_DLOCK_NAMES_LENGTH=Length of names in DLOCK files that will be taken to compare with ORD member names
#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)

View File

@@ -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');
}
};

View File

@@ -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');
}
};

View File

@@ -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

View File

@@ -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

View File

@@ -13,6 +13,8 @@ const {
addFees,
checkProcessingStatus,
getPracticeSummaryReport,
deleteFees,
updateFees
} = require('../controllers/integration');
const { calculateDoorLockCharges } = require('../services/integration/doorLockCharges');
@@ -34,6 +36,8 @@ router.get('/integration/report/allIncidents/:startDate/:endDate', getAllInciden
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);
@@ -42,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

@@ -14,11 +14,12 @@ const {
VALID_CSV_HEADERS,
doorLockEvents,
csvParserErrors,
TRIM_DLOCK_NAMES_LENGTH
} = require('../../constants/constants');
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 = /\[(.*?)\]/;
@@ -53,7 +54,12 @@ const parseDoorLockDataFile = (file) => {
const membersMap = {};
const unknownMembersMap = {};
allMembers.forEach((member) => membersMap[member.name] = member);
allMembers.forEach((member) => {
const limitedMemberName = member.name ? member.name.substring(0, TRIM_DLOCK_NAMES_LENGTH) || undefined : undefined;
if (limitedMemberName){
membersMap[limitedMemberName] = member;
}
});
const mappingFromFileName = extractMappingFromFileName(file.name);
const mappingObject = checkIfMappingExsists(mappingFromFileName, mappings);
@@ -110,18 +116,20 @@ const parseDoorLockDataFile = (file) => {
const firstEntry = results[i];
const secondEntry = results[i+1];
if (firstEntry && (firstEntry.event === USER_ENTRY_EVENT)){
const memberObject = membersMap[firstEntry.name];
const trimmedName = firstEntry && firstEntry.name ? firstEntry.name.substring(0, TRIM_DLOCK_NAMES_LENGTH) || undefined : undefined;
if (firstEntry && trimmedName && (firstEntry.event === USER_ENTRY_EVENT)){
const memberObject = membersMap[trimmedName];
if (!memberObject){
//Check if member is already labeled as unknown
const unknownMember = unknownMembersMap[firstEntry.name];
const unknownMember = unknownMembersMap[trimmedName];
if (!unknownMember){
unknownMembersMap[firstEntry.name] = firstEntry.name;
unknownMembersMap[trimmedName] = trimmedName;
unknownMembersToReport.push({
error: csvParserErrors.UNKNOWN_MEMBER,
details: firstEntry.name,
details: trimmedName,
file: file.name,
});
}
@@ -137,7 +145,7 @@ const parseDoorLockDataFile = (file) => {
//Verify that member is registered in OfficeRnD system
if (memberObject){
const entryData = {
memberName: firstEntry.name,
memberName: trimmedName,
memberNumber: firstEntry['user no'],
memberId: memberObject.memberId,
timestamp,
@@ -197,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,
@@ -222,7 +238,25 @@ const getUnlockEntryForReservation = (reservation, previousReservation) => {
const entriesBeforeReservationStart = entries.filter((entry) => moment.utc(entry.timestamp).isBefore(reservation.start));
// if (memberId === '5ce785af422bdd00967fb781') {
// console.log('Start : ', moment.tz(reservation.start, UI_TIMEZONE).format('DD.MM HH:mm'));
// console.log('End : ', moment.tz(reservation.end, UI_TIMEZONE).format('DD.MM HH:mm'));
// console.log('\tPrevious reservation ');
// console.log('\tStart : ', previousReservation ? moment.tz(previousReservation.start, UI_TIMEZONE).format('DD.MM HH:mm') : '-');
// console.log('\tEnd : ', previousReservation ? moment.tz(previousReservation.end, UI_TIMEZONE).format('DD.MM HH:mm') : '-');
// console.log('\t---------------------------');
// console.log('\tSearch for entries : ');
// console.log('\tFrom : ', fromTimestamp ? moment.tz(fromTimestamp, UI_TIMEZONE).format('DD.MM HH:mm') : '-');
// console.log('\tTo : ', toTimestamp ? moment.tz(toTimestamp, UI_TIMEZONE).format('DD.MM HH:mm') : '-');
// console.log('\t---------------------------');
// console.log('\tEntries before reservation start : ');
// }
entriesBeforeReservationStart.forEach((entry) => {
// if (memberId === '5ce785af422bdd00967fb781') {
// console.log('\t', entry.event, '\t', moment.tz(entry.timestamp, UI_TIMEZONE).format('DD.MM HH:mm'));
// }
if (!eventFound) {
if (entry.event === doorLockEvents.USER_UNLOCKED) {
if (pairedLockEntry) {
@@ -240,13 +274,25 @@ const getUnlockEntryForReservation = (reservation, previousReservation) => {
});
if (eventFound){
// if (memberId === '5ce785af422bdd00967fb781') {
// console.log('\t=> FOUND UNLOCK ENTRY - NO NEED TO LOOK AFTER <=');
// }
resolve(candidateUnlockEntry);
} else {
candidateUnlockEntry = null;
const numberOfEntriesLeft = entries.length - entriesBeforeReservationStart.length;
const entriesAfterReservationStart = entries.slice(0, numberOfEntriesLeft);
const invertedEntriesAfterReservationStart = entriesAfterReservationStart.reverse();
// if (memberId === '5ce785af422bdd00967fb781') {
// console.log('\t-----------------------------');
// console.log('\tEntries after reservation start : ');
// }
invertedEntriesAfterReservationStart.forEach((entry) => {
// if (memberId === '5ce785af422bdd00967fb781') {
// console.log('\t', entry.event, '\t', moment.tz(entry.timestamp, UI_TIMEZONE).format('DD.MM HH:mm'));
// }
entriesAfterReservationStart.forEach((entry) => {
if (!eventFound) {
if (entry.event === doorLockEvents.USER_UNLOCKED) {
eventFound = true;
@@ -287,7 +333,7 @@ const getLockEntryForReservation = (reservation, nextReservation) => {
resourceId,
};
const order = [['timestamp', 'ASC']];
const order = [['timestamp', 'DESC']];
db.doorLockEvent.findAll({
attributes,
@@ -295,47 +341,31 @@ const getLockEntryForReservation = (reservation, nextReservation) => {
order,
})
.then((entries) => {
let candidateLockEntry = null;
let pairedUnlockEntry = null;
let eventFound = false;
const entriesAfterReservationEnd = entries.filter((entry) => moment.utc(entry.timestamp).isAfter(reservation.end));
const entriesBeforeReservationEnd = entries.filter(entry => moment.utc(entry.timestamp).isBefore(reservation.end));
entriesAfterReservationEnd.forEach((entry) => {
if (!eventFound) {
if (entry.event === doorLockEvents.USER_LOCKED) {
if (pairedUnlockEntry) {
pairedUnlockEntry = null;
candidateLockEntry = null;
} else {
candidateLockEntry = entry;
eventFound = true;
}
}
if (entry.event === doorLockEvents.USER_UNLOCKED){
pairedUnlockEntry = entry;
}
if (entriesBeforeReservationEnd.length > 0){
const lastEntryInReservationTime = entriesBeforeReservationEnd[0];
if (lastEntryInReservationTime.event === doorLockEvents.USER_LOCKED){
resolve(lastEntryInReservationTime);
return;
}
});
if (eventFound){
resolve(candidateLockEntry);
} else {
candidateLockEntry = null;
const numberOfEntriesLeft = entries.length - entriesAfterReservationEnd.length;
const entriesBeforeReservationEnd = entries.slice(0, numberOfEntriesLeft);
entriesBeforeReservationEnd.reverse().forEach((entry) => {
if (!eventFound) {
if (entry.event === doorLockEvents.USER_LOCKED) {
eventFound = true;
candidateLockEntry = entry;
}
}
});
resolve(candidateLockEntry);
}
// Phase 2
const numberOfEntriesLeft = entries.length - entriesBeforeReservationEnd.length;
const entriesAfterReservationEnd = entries.slice(0, numberOfEntriesLeft).reverse();
if (entriesAfterReservationEnd.length > 0){
const firstEntryAfterReservation = entriesAfterReservationEnd[0];
if (firstEntryAfterReservation.event === doorLockEvents.USER_LOCKED){
resolve(firstEntryAfterReservation);
return;
}
}
resolve(null);
})
.catch((error) => reject(error));
});
@@ -343,7 +373,7 @@ const getLockEntryForReservation = (reservation, nextReservation) => {
const getEntriesBetween = (fromTimestamp, toTimestamp, resourceId) => {
return new Promise((resolve, reject) => {
if (!fromTimestamp || !toTimestamp || !resourceId){
if (!resourceId){
resolve([]);
}else {
const andTimestampFilters = [];
@@ -395,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

@@ -146,7 +146,7 @@ const getIncidentsFromChanges = (changes) => {
reservationsForAdditionalCheck.push(reservationId);
}
} else {
console.log('\t\tNo');
// console.log('\t\tNo');
// Reservation moved to another day
// Add cancellation charge
const chargeFee = oldReservationLength * reservationHourlyRate * BOOKING_CHANGE_PERCENTAGE_CHARGE / 100;
@@ -274,6 +274,31 @@ const deleteBookingChangeIncidents = (incidents) => {
return Promise.all(asyncActions);
};
const deleteBookingChangeIncidentsById = (incidentIds) => {
const filters = {
id: {
[Op.in]: 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,
@@ -281,4 +306,6 @@ module.exports = {
getShorteningIncidentsForReservationId,
getReservationsIncidentsForRemoval,
deleteBookingChangeIncidents,
deleteBookingChangeIncidentsById,
updateBookingChangeIncidentsById
};

View File

@@ -88,7 +88,24 @@ const getAllBookingsForMembersInDateRange = (dateRange, memberIds) => {
});
};
const deleteBookingsRemovedFromORD = (reservationIds) => {
if (!Array.isArray(reservationIds)){
return 0;
}
const filter = {
reservationId: {
[Op.notIn]: reservationIds
}
};
return db.bookingReservation.destroy({
where: filter
});
};
module.exports = {
getActiveBookingsForMembersInDateRange,
getAllBookingsForMembersInDateRange,
deleteBookingsRemovedFromORD
};

View File

@@ -3,6 +3,7 @@
const { fetchAllBookings, bulkWriteReservationsWithChangesTracking } = require('../officeRnD/bookings');
const { fetchResources } = require('../officeRnD/resources');
const { fetchRates } = require('../officeRnD/rates');
const { deleteBookingsRemovedFromORD } = require('./bookings');
const { officeRnDAPIErrors } = require('../../constants/constants');
const {
getIncidentsFromChanges,
@@ -23,23 +24,38 @@ const checkBookingChanges = () => {
const ratesMap = {};
rates.forEach(rate => {
const { rateId, price } = rate;
ratesMap[rateId] = price;
const { rateId, price, weekendPrice } = rate;
ratesMap[rateId] = {
price,
weekendPrice
};
});
const resourcesMap = {};
resources.forEach(resource => {
const { resourceId, rate } = resource;
resource.price = ratesMap[rate] || 0;
resource.price = ratesMap[rate] || {price: 0, weekendPrice: 0};
resourcesMap[resourceId] = resource;
});
bulkWriteReservationsWithChangesTracking(reservations, resourcesMap)
.then((changes) => {
const reservationsInORD = [];
reservations.forEach(reservation => {
const { reservationId } = reservation;
reservationsInORD.push(reservationId);
});
const asyncActions = [deleteBookingsRemovedFromORD(reservationsInORD), bulkWriteReservationsWithChangesTracking(reservations, resourcesMap)];
Promise.all(asyncActions)
.then((asyncActionResults) => {
const changes = asyncActionResults[1];
// console.log(changes);
bulkWriteChanges(changes)
.then(() => {
getIncidentsFromChanges(changes)
.then(({incidents, reservationsForAdditionalCheck}) => {
// console.log('=====INCIDENTS=====');
// console.log(incidents);
bulkWriteBookingChangeIncidents(incidents)
.then(() => {
getReservationsIncidentsForRemoval(reservationsForAdditionalCheck)

View File

@@ -2,6 +2,7 @@
const moment = require('moment-timezone');
const db = require('../../models/index');
const Op = require('sequelize').Op;
const {
doorLockEvents,
@@ -9,16 +10,18 @@ const {
unlockedIncidentLevelsPrices,
UNSCHEDULED_CHARGE_PRICE,
MAX_BACK_TO_BACK_DIFFERENCE,
UNSCHEDULED_USE_INITIAL_TIME_SEGMENT_LENGTH,
UNSCHEDULED_TIME_RESOLUTION,
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'];
const filters = {
memberId
memberId,
deleted: false
};
const order = [['unlockTimestamp', 'DESC']];
@@ -48,6 +51,7 @@ const insertUnscheduledIncidents = (incidents) => {
totalChargeFee,
unlockTimestamp,
lockTimestamp,
deleted: false
};
asyncJobs.push(db.unscheduledIncident.findOrCreate({
@@ -67,6 +71,35 @@ 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 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) => {
@@ -82,6 +115,7 @@ const insertUnlockedIncidents = (incidents) => {
unlockTimestamp,
incidentLevel,
incidentLevelPrice,
deleted: false
};
asyncJobs.push(db.unlockedIncident.findOrCreate({
@@ -92,7 +126,6 @@ const insertUnlockedIncidents = (incidents) => {
bookingStart: start,
bookingEnd: end,
unlockTimestamp,
incidentLevel,
},
defaults: {...incidentForDB},
}));
@@ -101,6 +134,31 @@ 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 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) => {
@@ -139,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,
@@ -154,8 +212,8 @@ const setUnlockedIncidentsLevel = (incidents) => {
formattedIncident.incidentLevel = unlockedIncidentLevelsPrices.UNLOCKED_0.title;
formattedIncident.incidentLevelPrice = unlockedIncidentLevelsPrices.UNLOCKED_0.price;
} else {
const lastIncidentTime = moment.utc(memberLastIncident.incidentTimestamp).startOf('month');
const currentIncidentTime = moment.utc(incident.unlockTimestamp).startOf('month');
const lastIncidentTime = moment.tz(memberLastIncident.incidentTimestamp, UI_TIMEZONE).startOf('month');
const currentIncidentTime = moment.tz(incident.unlockTimestamp, UI_TIMEZONE).startOf('month');
const timeDiff = Math.abs(lastIncidentTime.diff(currentIncidentTime, 'months'));
if (timeDiff >= (parseInt(process.env.UNLOCK_STREAK_REPAIR_AFTER) || 6)){
@@ -215,7 +273,7 @@ const analyseReservation = (reservation) => {
const previousReservationEnd = moment.utc(previousReservation.end);
timeDifferenceFromPreviousReservation = currentReservationStart.diff(previousReservationEnd, 'minutes');
}
const previousReservationIsBackToBack = timeDifferenceFromPreviousReservation < MAX_BACK_TO_BACK_DIFFERENCE;
const previousReservationIsBackToBack = timeDifferenceFromPreviousReservation <= MAX_BACK_TO_BACK_DIFFERENCE;
let timeDifferenceBeforeNextReservation = MAX_BACK_TO_BACK_DIFFERENCE + 100;
@@ -223,14 +281,17 @@ const analyseReservation = (reservation) => {
const nextReservationStart = moment.utc(nextReservation.start);
timeDifferenceBeforeNextReservation = nextReservationStart.diff(currentReservationEnd, 'minutes');
}
const nextReservationIsBackToBack = timeDifferenceBeforeNextReservation < MAX_BACK_TO_BACK_DIFFERENCE;
const nextReservationIsBackToBack = timeDifferenceBeforeNextReservation <= MAX_BACK_TO_BACK_DIFFERENCE;
let timeDifferenceFromUnlockEntry = 0;
if (unlockEntry) {
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);
timeIntervalsToChargeBefore -= 1; // Remove first N minutes, N = UNSCHEDULED_TIME_RESOLUTION
const totalChargeFeeBefore = timeIntervalsToChargeBefore * UNSCHEDULED_CHARGE_PRICE;
const chargeBefore = totalChargeFeeBefore > 0;
@@ -239,10 +300,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);
timeIntervalsToChargeAfter -= 1; // Remove first N minutes, N = UNSCHEDULED_TIME_RESOLUTION
const totalChargeFeeAfter = timeIntervalsToChargeAfter * UNSCHEDULED_CHARGE_PRICE;
const chargeAfter = totalChargeFeeAfter > 0;
// const reservationMoment = moment.tz(reservation.start, reservation.timezone);
// if (reservation.memberId === '5ce785af422bdd00967fb781' && reservationMoment.isAfter('2019-11-21 00:00:16+00')) {
// console.log('\r\n\r\n==== ANALYSE RESERVATION ==== ');
// 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('\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,
@@ -295,135 +400,61 @@ const getIncidentData = (reservation) => {
timeIntervalsToChargeAfter,
} = result;
const incidents = [];
const incidentsAsyncJobs = [];
const { resourceId } = currentReservation;
// 0a. Check for unscheduled use between current and previous reservation
const analysePreviousJob = [];
if (previousReservation && !previousReservationIsBackToBack){
analysePreviousJob.push(analyseReservation(previousReservation));
}else {
analysePreviousJob.push(undefined);
}
// const reservationMoment = moment.tz(currentReservation.start, currentReservation.timezone);
// 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'));
// console.log('\t----------------------------------');
// console.log('\tFirst previous reservation : is back to back [', previousReservationIsBackToBack ? 'T' : 'F' , ']');
// 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 : is back to back [', nextReservationIsBackToBack ? 'T' : 'F', ']');
// 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');
// }
// }
incidentsAsyncJobs.push(
Promise.all(analysePreviousJob)
.then(([previousReservationResults]) => {
let fromTimestamp;
let toTimestamp;
//**********************
if (previousReservationResults){
const previousReservationLockEntry = previousReservationResults.lockEntry;
fromTimestamp = previousReservationLockEntry && previousReservationLockEntry.timestamp ?
previousReservationLockEntry.timestamp : previousReservation.end;
toTimestamp = unlockEntry && unlockEntry.timestamp ?
unlockEntry.timestamp : reservation.start;
}else{
fromTimestamp = undefined;
toTimestamp = unlockEntry && unlockEntry.timestamp ?
unlockEntry.timestamp : reservation.start;
}
incidentsAsyncJobs.push(
getEntriesBetween(fromTimestamp, toTimestamp, resourceId)
.then((entriesBetween) => {
incidentsAsyncJobs.push(
new Promise((resolve, reject) => {
let pairUnlockEntry = null;
let pairLockEntry = null;
entriesBetween.forEach((entry) => {
if (entry && entry.event){
switch(entry.event){
case doorLockEvents.USER_UNLOCKED:
if (!pairUnlockEntry){
pairUnlockEntry = entry;
}else{
const emptyReservation = {
reservationId: null,
start: null,
end: null,
};
incidents.push({
incidentType: incidentType.UNLOCKED_INCIDENT_STANDALONE,
reservation: emptyReservation,
unlockTimestamp: pairUnlockEntry.timestamp,
memberId: pairUnlockEntry.memberId,
resourceId,
});
pairLockEntry = null;
pairUnlockEntry = entry;
}
break;
case doorLockEvents.USER_LOCKED:
if (pairUnlockEntry && !pairLockEntry){
pairLockEntry = entry;
const emptyReservation = {
reservationId: null,
start: null,
end: null,
};
const unlockMoment = moment.utc(pairUnlockEntry.timestamp);
const lockMoment = moment.utc(pairLockEntry.timestamp);
if (lockMoment.tz(UI_TIMEZONE).isSame(unlockMoment.tz(UI_TIMEZONE), 'day')){
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: pairUnlockEntry.timestamp,
lockTimestamp: pairLockEntry.timestamp,
memberId: pairUnlockEntry.memberId,
resourceId,
chargePrice: UNSCHEDULED_CHARGE_PRICE,
timeIntervalsToCharge,
totalChargeFee,
});
}
}else{
const emptyReservation = {
reservationId: null,
start: null,
end: null,
};
incidents.push({
incidentType: incidentType.UNLOCKED_INCIDENT_STANDALONE,
reservation: emptyReservation,
unlockTimestamp: pairUnlockEntry.timestamp,
memberId: pairUnlockEntry.memberId,
resourceId,
});
}
pairUnlockEntry = null;
pairLockEntry = null;
}else{
if (!pairUnlockEntry){
pairLockEntry = entry;
//Only lock entry, ignore now
}
pairLockEntry = null;
pairUnlockEntry = null;
}
}
}
});
resolve(null);
}));
})
.catch((error) => reject(error)));
})
.catch((error) => reject(error)));
const emptyReservation = {
reservationId: null,
start: null,
end: null,
};
// 1. Check if member entered before reservation start time
// if (currentReservation.memberId === '5ce785af422bdd00967fb781' && reservationMoment.isAfter('2019-12-01 00:00:16+00')) {
// 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) {
// if (currentReservation.memberId === '5ce785af422bdd00967fb781' && reservationMoment.isAfter('2019-12-01 00:00:16+00')) {
// console.log('\tIncident : YES');
// }
incidents.push({
incidentType: incidentType.UNSCHEDULED_INCIDENT_BEFORE_RESERVATION,
reservation,
@@ -435,10 +466,23 @@ const getIncidentData = (reservation) => {
timeIntervalsToCharge: timeIntervalsToChargeBefore,
totalChargeFee: totalChargeFeeBefore,
});
}else{
// if (currentReservation.memberId === '5ce785af422bdd00967fb781' && reservationMoment.isAfter('2019-12-01 00:00:16+00')) {
// console.log('\tIncident : NO');
// }
}
// 2. Check if member left after reservation end time
// if (currentReservation.memberId === '5ce785af422bdd00967fb781' && reservationMoment.isAfter('2019-12-01 00:00:16+00')) {
// 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) {
// if (currentReservation.memberId === '5ce785af422bdd00967fb781' && reservationMoment.isAfter('2019-12-01 00:00:16+00')) {
// console.log('\tIncident : YES');
// }
incidents.push({
incidentType: incidentType.UNSCHEDULED_INCIDENT_AFTER_RESERVATION,
reservation,
@@ -450,18 +494,108 @@ const getIncidentData = (reservation) => {
timeIntervalsToCharge: timeIntervalsToChargeAfter,
totalChargeFee: totalChargeFeeAfter,
});
}else{
// if (currentReservation.memberId === '5ce785af422bdd00967fb781' && reservationMoment.isAfter('2019-12-01 00:00:16+00')) {
// console.log('\tIncident : NO');
// }
}
// 3. Check if member forgot to lock the door
// if (currentReservation.memberId === '5ce785af422bdd00967fb781' && reservationMoment.isAfter('2019-12-01 00:00:16+00')) {
// 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);
// }
let forgotToLockAsyncCheck;
let asyncUnscheduledUseTest = [];
if (!lockEntry && !nextReservationIsBackToBack){
const emptyReservation = {
reservationId: null,
start: null,
end: null,
// 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 (unlockEntry){
// if (currentReservation.memberId === '5ce785af422bdd00967fb781' && reservationMoment.isAfter('2019-12-01 00:00:16+00')) {
// console.log('\tIncident : YES [#1]');
// }
incidents.push({
incidentType: incidentType.UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION,
unlockTimestamp: unlockEntry.timestamp,
@@ -469,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 :
@@ -480,29 +616,203 @@ const getIncidentData = (reservation) => {
if (previousReservationIsBackToBack){
// To ensure that this is last reservation in block (there is previous but no next reservation back to back)
// Now, just check if member actually locked before in the reservation time
incidentsAsyncJobs.push(
getLastEntryForReservation(reservation)
forgotToLockAsyncCheck = getLastEntryForReservation(reservation);
forgotToLockAsyncCheck
.then((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,
});
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)));
.catch((error) => reject(error));
}
}
}
Promise.all(incidentsAsyncJobs)
.then(() => {
resolve(incidents);
// 4. Check for unscheduled use between current and previous reservation
const analysePreviousJob = [];
if (previousReservation && !previousReservationIsBackToBack){
analysePreviousJob.push(analyseReservation(previousReservation));
}else {
analysePreviousJob.push(undefined);
}
Promise.all(analysePreviousJob)
.then(([previousReservationResults]) => {
let fromTimestamp;
let toTimestamp;
if (previousReservationResults){
const previousReservationLockEntry = previousReservationResults.lockEntry;
const previousReservationUnlockEntry = previousReservationResults.unlockEntry;
//Special check for bookings with break between
// 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;
toTimestamp = unlockEntry && unlockEntry.timestamp ?
unlockEntry.timestamp : reservation.start;
}else{
fromTimestamp = undefined;
toTimestamp = unlockEntry && unlockEntry.timestamp ?
unlockEntry.timestamp : reservation.start;
}
// Let's skip this if previous reservation is back-to-back
let getEntriesAsyncCheck;
if (previousReservation && previousReservationIsBackToBack){
//Skip
}else{
getEntriesAsyncCheck = getEntriesBetween(fromTimestamp, toTimestamp, resourceId);
}
//Now wait for "forgotToLockAsyncCheck", and possible "getEntriesBetween" to finish
Promise.all([forgotToLockAsyncCheck, getEntriesAsyncCheck, asyncUnscheduledUseTest])
.then((asyncActionResults) => {
const entriesBetween = asyncActionResults[1];
if (Array.isArray(entriesBetween)){
let pairUnlockEntry = null;
let pairLockEntry = null;
//Inspect all entries and insert detected incidents
entriesBetween.forEach((entry) => {
// console.log('\tEvent : ', entry.event);
// console.log('\tEvent : ', entry.timestamp ? moment.tz(entry.timestamp, UI_TIMEZONE).format('DD.MM, HH:mm') : '-');
if (entry && entry.event){
switch(entry.event){
case doorLockEvents.USER_UNLOCKED:
if (!pairUnlockEntry){
pairUnlockEntry = entry;
}else{
incidents.push({
incidentType: incidentType.UNLOCKED_INCIDENT_STANDALONE,
reservation: emptyReservation,
unlockTimestamp: pairUnlockEntry.timestamp,
memberId: pairUnlockEntry.memberId,
resourceId,
});
pairLockEntry = null;
pairUnlockEntry = entry;
}
break;
case doorLockEvents.USER_LOCKED:
if (pairUnlockEntry && !pairLockEntry){
pairLockEntry = entry;
const unlockMoment = moment.utc(pairUnlockEntry.timestamp);
const lockMoment = moment.utc(pairLockEntry.timestamp);
const entriesOnSameDay = lockMoment.tz(UI_TIMEZONE).isSame(unlockMoment.tz(UI_TIMEZONE), 'day');
const sameMember = pairUnlockEntry.memberId === pairLockEntry.memberId;
if ((entriesOnSameDay && !sameMember) || (!entriesOnSameDay)){
incidents.push({
incidentType: incidentType.UNLOCKED_INCIDENT_STANDALONE,
reservation: emptyReservation,
unlockTimestamp: pairUnlockEntry.timestamp,
memberId: pairUnlockEntry.memberId,
resourceId,
});
}else if (entriesOnSameDay && sameMember){
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: pairUnlockEntry.timestamp,
lockTimestamp: pairLockEntry.timestamp,
memberId: pairUnlockEntry.memberId,
resourceId,
chargePrice: UNSCHEDULED_CHARGE_PRICE,
timeIntervalsToCharge,
totalChargeFee,
});
}
}
pairUnlockEntry = null;
pairLockEntry = null;
}else{
pairLockEntry = null;
pairUnlockEntry = null;
}
}
}
});
if (pairUnlockEntry){
incidents.push({
incidentType: incidentType.UNLOCKED_INCIDENT_STANDALONE,
reservation: emptyReservation,
unlockTimestamp: pairUnlockEntry.timestamp,
memberId: pairUnlockEntry.memberId,
resourceId,
});
pairLockEntry = null;
pairUnlockEntry = null;
}
}
resolve(incidents);
})
.catch(error => reject(error));
})
.catch((error) => reject(error));
.catch(error => reject(error));
//
// Promise.all(incidentsAsyncJobs)
// .then(() => {
// // console.log('\tDone with Async jobs for reservation ');
// // console.log('\t\tStart : ', reservationMoment.format('DD.MM, HH:mm'));
// // console.log('\t\tEnd : ', moment.tz(currentReservation.end, currentReservation.timezone).format('DD.MM, HH:mm'));
//
// setTimeout(() => {resolve(incidents)}, 10000);
// // resolve(incidents);
// })
// .catch((error) => reject(error));
})
.catch((error) => reject(error));
@@ -573,5 +883,9 @@ const calculateDoorLockCharges = () => {
};
module.exports = {
calculateDoorLockCharges
calculateDoorLockCharges,
deleteUnlockedIncidentsById,
deleteUnscheduledIncidentsById,
updateUnlockedIncidentsById,
updateUnscheduledIncidentsById
};

View File

@@ -17,6 +17,7 @@ const createFeeFromIncident = (incident) => {
memberId,
officeId,
officeName,
officeSlug,
resourceName,
oldResourceName,
newResourceName,
@@ -45,6 +46,10 @@ const createFeeFromIncident = (incident) => {
let bookingTimeExplanation = '';
let incidentTimeExplanation = '';
let additionalDateTimeExplanation = '';
let spacing = '';
let roomExplanation = '';
let dateExplanation = '';
@@ -62,7 +67,8 @@ const createFeeFromIncident = (incident) => {
switch (incidentTypeNumber) {
case incidentType.UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION:
roomExplanation = resourceName;
spacing = ' ';
roomExplanation = resourceName || 'Unknown';
dateExplanation = bookingStartMoment.clone().startOf('day').format('MMM DD');
bookingTimeExplanation = `[${bookingStartMoment.clone().format('HH:mm')} to ${bookingEndMoment.clone().format('HH:mm')}]`;
incidentTimeExplanation = `unlock : ${unlockMoment.clone().format('HH:mm')}`;
@@ -74,7 +80,8 @@ const createFeeFromIncident = (incident) => {
quantity = 1.00;
break;
case incidentType.UNSCHEDULED_INCIDENT_BEFORE_RESERVATION:
roomExplanation = resourceName;
spacing = ' ';
roomExplanation = resourceName || 'Unknown';
dateExplanation = bookingStartMoment.clone().startOf('day').format('MMM DD');
bookingTimeExplanation = `[${bookingStartMoment.clone().format('HH:mm')} to ${bookingEndMoment.clone().format('HH:mm')}]`;
incidentTimeExplanation = `unlock : ${unlockMoment.clone().format('HH:mm')}`;
@@ -86,7 +93,8 @@ const createFeeFromIncident = (incident) => {
quantity = +timeIntervalsToCharge.toFixed(2);
break;
case incidentType.UNSCHEDULED_INCIDENT_AFTER_RESERVATION:
roomExplanation = resourceName;
spacing = ' ';
roomExplanation = resourceName || 'Unknown';
dateExplanation = bookingStartMoment.clone().startOf('day').format('MMM DD');
bookingTimeExplanation = `[${bookingStartMoment.clone().format('HH:mm')} to ${bookingEndMoment.clone().format('HH:mm')}]`;
incidentTimeExplanation = `lock : ${lockMoment.clone().format('HH:mm')}`;
@@ -98,7 +106,8 @@ const createFeeFromIncident = (incident) => {
quantity = +timeIntervalsToCharge.toFixed(2);
break;
case incidentType.UNLOCKED_INCIDENT_STANDALONE:
roomExplanation = resourceName;
spacing = ' ';
roomExplanation = resourceName || 'Unknown';
dateExplanation = unlockMoment.clone().startOf('day').format('MMM DD');
bookingTimeExplanation = `[${unlockMoment.clone().format('HH:mm')} to ${lockMoment.clone().format('HH:mm')}]`;
incidentTimeExplanation = `unlock : ${unlockMoment.clone().format('HH:mm')}`;
@@ -110,7 +119,8 @@ const createFeeFromIncident = (incident) => {
quantity = 1.00;
break;
case incidentType.UNSCHEDULED_INCIDENT_STANDALONE:
roomExplanation = resourceName;
spacing = ' ';
roomExplanation = resourceName || 'Unknown';
dateExplanation = unlockMoment.clone().startOf('day').format('MMM DD');
bookingTimeExplanation = `[${unlockMoment.clone().format('HH:mm')} to ${lockMoment.clone().format('HH:mm')}]`;
//incidentTimeExplanation = `unlock : ${unlockMoment.clone().format('HH:mm')}, lock : ${lockMoment.clone().format('HH:mm')}`;
@@ -121,16 +131,18 @@ const createFeeFromIncident = (incident) => {
quantity = +timeIntervalsToCharge.toFixed(2);
break;
case incidentType.BOOKING_MOVED_TO_ANOTHER_DAY:
spacing = ' ';
// if (oldResourceName !== newResourceName){
// roomExplanation = `${oldResourceName} -> ${newResourceName}`;
// }else{
// roomExplanation = oldResourceName;
// }
roomExplanation = newResourceName;
roomExplanation = newResourceName || 'Unknown';
// dateExplanation = `${oldBookingStartMoment.clone().format('ddd, MMM DD')} -> ${newBookingStartMoment.clone().format('ddd, MMM DD')}`;
dateExplanation = `${newBookingStartMoment.clone().format('MMM DD')}`;
bookingTimeExplanation = `[${newBookingStartMoment.clone().format('HH:mm')} to ${newBookingEndMoment.clone().format('HH:mm')}]`;
dateExplanation = `${oldBookingStartMoment.clone().format('MMM DD')}`;
bookingTimeExplanation = `[${oldBookingStartMoment.clone().format('HH:mm')} to ${oldBookingEndMoment.clone().format('HH:mm')}]`;
additionalDateTimeExplanation = ` -> ${newBookingStartMoment.clone().format('MMM DD')} [${newBookingStartMoment.clone().format('HH:mm')} to ${newBookingEndMoment.clone().format('HH:mm')}]`;
incidentTimeExplanation = `moved on : ${incidentTimestampMoment.clone().format('MMM DD, HH:mm')}`;
incidentExplanation += `, ${incidentTimeExplanation}`;
@@ -140,26 +152,30 @@ const createFeeFromIncident = (incident) => {
quantity = 1.00;
break;
case incidentType.BOOKING_SHORTENED:
spacing = ' ';
// if (oldResourceName !== newResourceName){
// roomExplanation = `${oldResourceName} -> ${newResourceName}`;
// }else{
// roomExplanation = oldResourceName;
// }
roomExplanation = newResourceName;
roomExplanation = newResourceName || 'Unknown';
// dateExplanation = `${oldBookingStartMoment.clone().format('ddd, MMM DD')}`;
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 = `shortened on : ${incidentTimestampMoment.clone().format('MMM DD, HH:mm')}`;
incidentExplanation += `, ${incidentTimeExplanation}`;
incidentTimeExplanation = `reservation shortened from [${originalBookingExplanation}] on [${incidentTimestampMoment.clone().format('MMM DD, HH:mm')}]`;
incidentExplanation = `${incidentTimeExplanation}`;
date = incidentTimestampMoment.clone().startOf('day').format();
date = newBookingStartMoment.clone().utc(UI_TIMEZONE).startOf('day').format();
price = +totalChargeFee.toFixed(2);
quantity = 1.00;
break;
case incidentType.BOOKING_CANCELED_LATE:
roomExplanation = oldResourceName;
spacing = ' ';
roomExplanation = oldResourceName || 'Unknown';
// dateExplanation = `${oldBookingStartMoment.clone().format('ddd, MMM DD')}`;
dateExplanation = `${oldBookingStartMoment.clone().format('MMM DD')}`;
bookingTimeExplanation = `[${oldBookingStartMoment.clone().format('HH:mm')} to ${oldBookingEndMoment.clone().format('HH:mm')}]`;
@@ -173,7 +189,7 @@ const createFeeFromIncident = (incident) => {
break;
}
const formattedName = `${dateExplanation} ${bookingTimeExplanation} ${roomExplanation}, ${officeName}, ${incidentExplanation}`;
const formattedName = `${officeSlug}, ${dateExplanation} ${bookingTimeExplanation}${additionalDateTimeExplanation}${spacing}${roomExplanation}, ${incidentExplanation}`;
return {
name: formattedName,
@@ -202,14 +218,14 @@ const createFeeFromBooking = (booking, resourceMappings) => {
const endMoment = moment.tz(end, DEFAULT_DATE_FORMAT, timezone);
const reservationLength = endMoment.diff(startMoment, 'hours', true);
const officeName = officesMap[officeId].officeName || 'Unknown';
const officeSlug = officesMap[officeId].officeSlug || 'Unknown';
const resourceName = resourcesMap[resourceId].resourceName || 'Unknown';
const formattedDate = startMoment.clone().startOf('day').format('MMM DD');
const formattedStartTime = startMoment.format('HH:mm');
const formattedEndTime = endMoment.format('HH:mm');
const formattedName = `${formattedDate} [${formattedStartTime} to ${formattedEndTime}] ${resourceName}, ${officeName}`;
const formattedName = `${officeSlug}, ${formattedDate} [${formattedStartTime} to ${formattedEndTime}] ${resourceName}`;
return {
name: formattedName,
@@ -228,9 +244,9 @@ const createNegativeFeeForDiscount = (memberData, dateRange) => {
const { totalBookedHours, totalChargedHours, totalBookingChargedFee } = bookingData;
const { memberId, officeId } = member;
let endDate = moment.utc().endOf('day').toISOString();
if (dateRange.endDate){
endDate = moment.utc(dateRange.endDate, DEFAULT_DATE_FORMAT).endOf('day').toISOString();
let dateForDiscount = moment.utc().subtract(1, 'month').startOf('month').toISOString();
if (dateRange.startDate){
dateForDiscount = moment.utc(dateRange.startDate, DEFAULT_DATE_FORMAT).startOf('month').toISOString();
}
let membershipFeeForDiscount = 0;
@@ -269,7 +285,7 @@ const createNegativeFeeForDiscount = (memberData, dateRange) => {
name: formattedName,
price: -discount.toFixed(2),
quantity: 1,
date: endDate,
date: dateForDiscount,
member: memberId,
team: null,
office: officeId,
@@ -330,7 +346,7 @@ const getMembersFeesForDateRange = (dateRange, memberIds) => {
allIncidents.forEach((incident) => {
const feeFromIncident = createFeeFromIncident(incident);
if (feeFromIncident){
allFees.push(createFeeFromIncident(incident));
allFees.push(feeFromIncident);
}
const incidentsValuableForDiscountCalculation = [
@@ -464,7 +480,7 @@ const getMembersFeesForDateRange = (dateRange, memberIds) => {
fee.team = memberIdTeamMappings[member] || null;
if (teamId){
//if member is part of the company, add name to the fee description/name
fee.name += `, ${memberName}`;
fee.name = `${memberName}, ${fee.name}`;
}
}
});

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');
@@ -18,7 +17,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 +77,9 @@ const getUnscheduledIncidents = (startDate, endDate, memberIds) => {
'totalChargeFee'
];
const filters = {};
const filters = {
deleted: false
};
if (startDate && endDate) {
const bookingStartCondition = {
@@ -218,18 +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,
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,
@@ -248,19 +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,
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,
@@ -285,13 +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 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,
@@ -300,10 +326,11 @@ const getAllIncidents = (dateRange, memberIds) => {
newResourceName,
officeId,
officeName,
oldBookingStart: formatTime(oldBookingStart),
oldBookingEnd: formatTime(oldBookingEnd),
newBookingStart: formatTime(newBookingStart),
newBookingEnd: formatTime(newBookingEnd),
officeSlug,
oldBookingStart: formatTime(oldBookingStart) || '-',
oldBookingEnd: formatTime(oldBookingEnd) || '-',
newBookingStart: formatTime(newBookingStart) || '-',
newBookingEnd: formatTime(newBookingEnd) || '-',
oldBookingStartRaw: oldBookingStart,
oldBookingEndRaw: oldBookingEnd,
newBookingStartRaw: newBookingStart,
@@ -311,7 +338,7 @@ const getAllIncidents = (dateRange, memberIds) => {
incidentType,
totalChargeFee: chargeFee,
deleted,
incidentTimestamp: formatTime(createdAt),
incidentTimestamp: formatTime(createdAt) || '-',
incidentTimestampRaw: createdAt,
});
});
@@ -332,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 = {};
@@ -386,6 +413,7 @@ const getMemberPracticeSummaryReport = (year) => {
getChargedCanceledReservations(reservationIdsForAdditionalData)
.then((incidents) => {
console.log('Charged canceled reservations ...');
incidents.forEach((incident) => {
const {memberId, oldBookingStart, oldBookingEnd} = incident.get();
@@ -503,15 +531,29 @@ const getMemberPracticeSummaryReport = (year) => {
const inactiveMemberIdsList = [];
memberIdsListFromReportMap.forEach((memberId) => {
if (membersMap[memberId].active){
activeMemberIdsList.push(memberId);
if (memberId){
if (membersMap[memberId] && membersMap[memberId].active){
activeMemberIdsList.push(memberId);
}else{
console.log('[Get Member Practice Summary Report] Unknown member ');
console.log('\tmemberId : ', memberId);
console.log('\tmembersMap[memberId] : ', membersMap[memberId]);
inactiveMemberIdsList.push(memberId);
}
}else{
inactiveMemberIdsList.push(memberId);
console.log('[Get Member Practice Summary Report] memberId is wrong : ', memberId);
}
});
const sortMemberIdsListByName = (memberId1, memberId2) => {
if (membersMap[memberId1].name > membersMap[memberId2].name){
const name1 = membersMap[memberId1] ? membersMap[memberId1].name || 'Unknown member' : null;
const name2 = membersMap[memberId2] ? membersMap[memberId2].name || 'Unknown member' : null;
if (!name1 || !name2){
return 0;
}
if (name1 > name2){
return 1;
}else{
return -1;

View File

@@ -5,7 +5,7 @@ const moment = require('moment-timezone');
const Op = require('sequelize').Op;
const { API } = require('../../helpers/api');
const { officeRnDAPIErrors, MAX_BACK_TO_BACK_DIFFERENCE } = require('../../constants/constants');
const { officeRnDAPIErrors, MAX_BACK_TO_BACK_DIFFERENCE, UI_TIMEZONE } = require('../../constants/constants');
const fetchAllBookings = () => {
return new Promise((resolve, reject) => {
@@ -14,24 +14,136 @@ const fetchAllBookings = () => {
const cleanedBookingReservations = [];
const bookingData = result && result.data ? result.data : [];
bookingData.forEach((fullBookingEntry) => {
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 bookingsToCreate = [];
const bookingIdsToRemove = [];
cleanedBookingReservations.push({
reservationId: fullBookingEntry['_id'],
memberId: fullBookingEntry.member,
officeId: fullBookingEntry.office,
resourceId: fullBookingEntry.resourceId,
start: fullBookingEntry.start.dateTime,
end: fullBookingEntry.end.dateTime,
timezone: fullBookingEntry.timezone,
canceled: fullBookingEntry.canceled || false,
hourlyRate,
});
bookingData.forEach(fullBookingEntry => {
if (!fullBookingEntry){
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
const member = fullBookingEntry.member ? fullBookingEntry.member : null;
const office = fullBookingEntry.office ? fullBookingEntry.office : null;
const resourceId = fullBookingEntry.resourceId ? fullBookingEntry.resourceId : null;
const team = fullBookingEntry.team ? fullBookingEntry.team : null;
const organization = fullBookingEntry.organization ? fullBookingEntry.organization : null;
const plan = fullBookingEntry.plan ? fullBookingEntry.plan : null;
const timezone = fullBookingEntry.timezone ? fullBookingEntry.timezone : UI_TIMEZONE;
const source = 'admin';
const startMoment = fullBookingEntry && fullBookingEntry.start && fullBookingEntry.start.dateTime ?
moment.utc(fullBookingEntry.start.dateTime) : null;
const endMoment = fullBookingEntry && fullBookingEntry.end && fullBookingEntry.end.dateTime ?
moment.utc(fullBookingEntry.end.dateTime) : null;
fees.forEach(fee => {
const dateMoment = fee.date ? moment.utc(fee.date) : null;
if (startMoment && endMoment && dateMoment){
const yearPart = dateMoment.year();
const monthPart = dateMoment.month();
const dayPart = dateMoment.date();
const newStartMoment = startMoment.clone().tz(fullBookingEntry.timezone).year(yearPart).month(monthPart).date(dayPart);
const newEndMoment = endMoment.clone().tz(fullBookingEntry.timezone).year(yearPart).month(monthPart).date(dayPart);
bookingsToCreate.push({
start: {
dateTime: newStartMoment.toISOString()
},
end: {
dateTime: newEndMoment.toISOString()
},
team,
member,
resourceId,
office,
source,
timezone,
organization,
plan
})
}
});
bookingIdsToRemove.push(fullBookingEntry['_id']);
}
});
resolve(cleanedBookingReservations);
//Here we now have possible bookings to create and then load again "check Booking changes"
if (bookingIdsToRemove.length > 0){
//First delete, wait until operation is done, than create bookings (to avoid conflicting date/time)
API.delete('bookings/?silent', { data: bookingIdsToRemove })
.then(() => {
//Now, insert new bookings
API.post('bookings/?silent', bookingsToCreate)
.then(() => {
//And fetch again all bookings
resolve(fetchAllBookings());
})
.catch((error) => {
console.log(officeRnDAPIErrors.FAILED_TO_CREATE_BOOKINGS);
console.log('Details : ', error);
reject(officeRnDAPIErrors.FAILED_TO_CREATE_BOOKINGS);
});
})
.catch(error => {
console.log(officeRnDAPIErrors.FAILED_TO_DELETE_BOOKINGS);
console.log('Details : ', error);
reject(officeRnDAPIErrors.FAILED_TO_DELETE_BOOKINGS);
});
}else{
bookingData.forEach((fullBookingEntry) => {
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;
const endMoment = fullBookingEntry && fullBookingEntry.end && fullBookingEntry.end.dateTime ?
moment.utc(fullBookingEntry.end.dateTime) : null;
// console.log('\r\n\r\nStart : ', startMoment.clone().tz(fullBookingEntry.timezone).format('DD.MM. HH:mm'), '[', startMoment.toISOString(),']');
// console.log('End : ', endMoment.clone().tz(fullBookingEntry.timezone).format('DD.MM. HH:mm'), '[', endMoment.toISOString(), ']');
// console.log('Fees : ');
if (startMoment && endMoment && !free && !tentative){
cleanedBookingReservations.push({
reservationId: fullBookingEntry['_id'],
memberId: fullBookingEntry.member,
officeId: fullBookingEntry.office,
resourceId: fullBookingEntry.resourceId,
start: startMoment.toISOString(),
end: endMoment.toISOString(),
timezone: fullBookingEntry.timezone,
canceled: fullBookingEntry.canceled || false,
hourlyRate,
});
}
});
resolve(cleanedBookingReservations);
}
})
.catch((error) => {
console.log(officeRnDAPIErrors.FAILED_TO_FETCH_BOOKINGS);
@@ -149,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 = {
@@ -193,7 +331,15 @@ const bulkWriteReservationsWithChangesTracking = (reservations, resourcesMap) =>
if (parseFloat(instance.previous('hourlyRate') > 0)) {
instance.setDataValue('hourlyRate', instance.previous('hourlyRate'));
}else{
const hourlyRate = resourceId ? resourcesMap[resourceId].price : 0;
//Determine if we should apply weekend price or work day price
const newStartWeekDay = moment.utc(instance.start).isoWeekday();
const isWeekend = newStartWeekDay > 5; //6 - Saturday, 7 - Sunday
let hourlyRate;
if (isWeekend){
hourlyRate = resourceId ? resourcesMap[resourceId].price.weekendPrice : 0;
}else{
hourlyRate = resourceId ? resourcesMap[resourceId].price.price : 0;
}
instance.setDataValue('hourlyRate', hourlyRate);
}
}
@@ -266,5 +412,6 @@ module.exports = {
getFirstNextBooking,
getFirstPreviousBooking,
getFirstReservationInBlock,
getLastReservationInBlock,
bulkWriteReservationsWithChangesTracking,
};

View File

@@ -3,14 +3,7 @@
const moment = require('moment-timezone');
const { API } = require('../../helpers/api');
const {
officeRnDAPIErrors,
DEFAULT_DATE_FORMAT,
UNPAID_FEE_STATUS,
CUSTOM_FEES_PREFIXES,
MAX_FEES_TO_DELETE,
FEES_DELETE_DELAY,
} = require('../../constants/constants');
const { officeRnDAPIErrors, DEFAULT_DATE_FORMAT, UNPAID_FEE_STATUS, CUSTOM_FEES_PREFIXES } = require('../../constants/constants');
const deleteFeesFromORD = (dateRange, memberIds) => {
return new Promise((resolve, reject) => {
@@ -29,9 +22,14 @@ const deleteFeesFromORD = (dateRange, memberIds) => {
const fetchedFees = feesResponse.data ? feesResponse.data : [];
const fetchedPlans = plansResponse.data ? plansResponse.data : [];
const planIsRateMap = {};
const manualFeeNames = [];
fetchedPlans.forEach((plan) => {
const { name } = plan;
const { name, _id, isRate } = plan;
planIsRateMap[_id] = !!isRate;
if (name && name.length > 0){
manualFeeNames.push(name);
}
@@ -47,18 +45,24 @@ const deleteFeesFromORD = (dateRange, memberIds) => {
const feeIdsToRemove = [];
fetchedFees.forEach((fee) => {
const { member, date, invoice, name } = fee;
const { member, date, invoice, name, plan } = fee;
const { status } = invoice;
const feeId = fee['_id'];
const feePlanIsRate = plan ? planIsRateMap[plan] : null;
const isDateInDateRange = startDate.isSameOrBefore(date) && endDate.isSameOrAfter(date);
let manuallyAddedFee = false;
if (manualFeeNames.indexOf(name) === -1){
if (name && name[0] && CUSTOM_FEES_PREFIXES.indexOf(name[0]) !== -1){
if (plan && !feePlanIsRate){
manuallyAddedFee = true;
}else{
if (manualFeeNames.indexOf(name) === -1){
if (name && name[0] && CUSTOM_FEES_PREFIXES.indexOf(name[0]) !== -1){
manuallyAddedFee = true;
}
}else{
manuallyAddedFee = true;
}
}else{
manuallyAddedFee = true;
}
const memberFeesShouldBeDeleted = filterByMemberIds ? memberIdsMap[member] : true;
@@ -70,28 +74,14 @@ const deleteFeesFromORD = (dateRange, memberIds) => {
}
});
const asyncDeleteCalls = [];
let i,j;
for (i=0, j=feeIdsToRemove; i<j; i+= MAX_FEES_TO_DELETE){
const feesSubset = feeIdsToRemove.slice(i, i+MAX_FEES_TO_DELETE);
const deleteFeesPromise = API.delete('fees', { data: feesSubset }).then(() => resolve(true)).catch((error) => {
console.log('[Delete Fees From ORD] Error deleting fees from ORD : ', error);
reject(officeRnDAPIErrors.FAILED_TO_DELETE_FEES);
});
asyncDeleteCalls.push(deleteFeesPromise);
//sleep for FEES_DELETE_DELAY ms
}
Promise.all(asyncDeleteCalls)
API.delete('fees/?silent', { data: feeIdsToRemove })
.then(() => {
resolve(feesToSkip);
})
.catch((error) => {
reject(error);
})
console.log('[Delete Fees From ORD] Error deleting fees from ORD : ', error);
reject(officeRnDAPIErrors.FAILED_TO_DELETE_FEES);
});
})
.catch((error) => {
console.log("[Delete Fees From ORD] Error fetching fees and plans from ORD : ", error);

View File

@@ -9,9 +9,17 @@ const fetchRates = () => {
const rates = result.data || [];
const cleanedRates = [];
rates.forEach(rate => {
const additionalRates = rate.rates;
let weekendRate = rate.price; //fallback price
additionalRates.forEach(additionalRate => {
if (additionalRate.isWeekendRate){
weekendRate = additionalRate.price;
}
});
cleanedRates.push({
rateId: rate['_id'],
price: rate.price,
weekendPrice: weekendRate
});
});
resolve(cleanedRates);

View File

@@ -15,6 +15,7 @@ const fetchOffices = () => {
cleanedOffices.push({
officeId: office['_id'],
officeName: office.name,
officeSlug: office.description,
});
});
resolve(cleanedOffices);