596 lines
25 KiB
JavaScript
596 lines
25 KiB
JavaScript
'use strict';
|
|
|
|
const moment = require('moment-timezone');
|
|
|
|
const db = require('../../models/index');
|
|
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 { getAllBookingsForYear } = require('./bookings');
|
|
const { fetchAllMembers } = require('../officeRnD/members');
|
|
const { fetchOffices, fetchResources } = require('../officeRnD/resources');
|
|
const { getChargedCanceledReservations } = require('../integration/bookingChangeCharges');
|
|
|
|
const getUnlockedIncidents = (startDate, endDate, memberIds) => {
|
|
const attributes = ['id', 'reservationId', 'memberId', 'resourceId', 'bookingStart', 'bookingEnd', 'unlockTimestamp', 'incidentLevel', 'incidentLevelPrice'];
|
|
|
|
const filters = {};
|
|
|
|
if (startDate && endDate) {
|
|
const bookingStartCondition = {
|
|
bookingStart: {
|
|
[Op.and]: {
|
|
[Op.gte]: startDate.toISOString(),
|
|
[Op.lte]: endDate.toISOString(),
|
|
}
|
|
}
|
|
};
|
|
|
|
const unlockTimestampCondition = {
|
|
unlockTimestamp: {
|
|
[Op.and]: {
|
|
[Op.gte]: startDate.toISOString(),
|
|
[Op.lte]: endDate.toISOString(),
|
|
}
|
|
}
|
|
};
|
|
|
|
const bookingStartOrUnlockTimestamp = {
|
|
[Op.or]: [bookingStartCondition, unlockTimestampCondition]
|
|
};
|
|
|
|
Object.assign(filters, bookingStartOrUnlockTimestamp);
|
|
}
|
|
|
|
if (memberIds.length > 0){
|
|
filters.memberId = {
|
|
[Op.in]: memberIds
|
|
};
|
|
}
|
|
|
|
return db.unlockedIncident.findAll({
|
|
attributes,
|
|
where: filters,
|
|
sort: [
|
|
['bookingStart', 'ASC']
|
|
]
|
|
});
|
|
};
|
|
|
|
const getUnscheduledIncidents = (startDate, endDate, memberIds) => {
|
|
const attributes = [
|
|
'id',
|
|
'reservationId',
|
|
'memberId',
|
|
'resourceId',
|
|
'bookingStart',
|
|
'bookingEnd',
|
|
'unlockTimestamp',
|
|
'lockTimestamp',
|
|
'timeIntervalsToCharge',
|
|
'chargePrice',
|
|
'totalChargeFee'
|
|
];
|
|
|
|
const filters = {};
|
|
|
|
if (startDate && endDate) {
|
|
const bookingStartCondition = {
|
|
bookingStart: {
|
|
[Op.and]: {
|
|
[Op.gte]: startDate.toISOString(),
|
|
[Op.lte]: endDate.toISOString(),
|
|
}
|
|
}
|
|
};
|
|
|
|
const unlockTimestampCondition = {
|
|
unlockTimestamp: {
|
|
[Op.and]: {
|
|
[Op.gte]: startDate.toISOString(),
|
|
[Op.lte]: endDate.toISOString(),
|
|
}
|
|
}
|
|
};
|
|
|
|
const bookingStartOrUnlockTimestamp = {
|
|
[Op.or]: [bookingStartCondition, unlockTimestampCondition]
|
|
};
|
|
|
|
Object.assign(filters, bookingStartOrUnlockTimestamp);
|
|
}
|
|
|
|
|
|
if (memberIds.length > 0){
|
|
filters.memberId = {
|
|
[Op.in]: memberIds
|
|
};
|
|
}
|
|
|
|
return db.unscheduledIncident.findAll({
|
|
attributes,
|
|
where: filters,
|
|
sort: [
|
|
['bookingStart', 'ASC']
|
|
]
|
|
});
|
|
};
|
|
|
|
const getBookingChangeIncidents = (startDate, endDate, memberIds) => {
|
|
const attributes = [
|
|
'id',
|
|
'reservationId',
|
|
'memberId',
|
|
'oldResourceId',
|
|
'newResourceId',
|
|
'oldBookingStart',
|
|
'oldBookingEnd',
|
|
'newBookingStart',
|
|
'newBookingEnd',
|
|
'incidentType',
|
|
'chargeFee',
|
|
'createdAt'
|
|
];
|
|
|
|
const filters = {};
|
|
|
|
if (startDate && endDate) {
|
|
filters.createdAt = {
|
|
[Op.and]: {
|
|
[Op.gte]: startDate.toISOString(),
|
|
[Op.lte]: endDate.toISOString(),
|
|
}
|
|
}
|
|
}
|
|
|
|
if (memberIds.length > 0){
|
|
filters.memberId = {
|
|
[Op.in]: memberIds
|
|
};
|
|
}
|
|
|
|
return db.bookingChangeIncident.findAll({
|
|
attributes,
|
|
where: filters,
|
|
sort: [
|
|
['createdAt', 'ASC']
|
|
]
|
|
});
|
|
};
|
|
|
|
const formatTime = (timestamp) => {
|
|
const momentObject = moment.tz(timestamp, UI_TIMEZONE);
|
|
if (momentObject.isValid()){
|
|
return momentObject.format('MM/DD/YYYY hh:mm a');
|
|
}else{
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getAllIncidents = (dateRange, memberIds) => {
|
|
return new Promise ((resolve, reject) => {
|
|
let startDate, endDate;
|
|
|
|
if (dateRange.startDate && dateRange.endDate){
|
|
startDate = moment.tz(dateRange.startDate, DEFAULT_DATE_FORMAT, UI_TIMEZONE).startOf('day');
|
|
endDate = moment.tz(dateRange.endDate, DEFAULT_DATE_FORMAT, UI_TIMEZONE).endOf('day');
|
|
|
|
if (!startDate.isValid() || !endDate.isValid() || endDate.isBefore(startDate)){
|
|
reject(integrationServiceErrors.INVALID_DATE_RANGE);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const dataFetchJobs = [
|
|
fetchAllMembers(),
|
|
fetchOffices(),
|
|
fetchResources(),
|
|
getUnlockedIncidents(startDate, endDate, memberIds),
|
|
getUnscheduledIncidents(startDate, endDate, memberIds),
|
|
getBookingChangeIncidents(startDate, endDate, memberIds)
|
|
];
|
|
|
|
Promise.all(dataFetchJobs)
|
|
.then((data) => {
|
|
const members = data[0];
|
|
const offices = data[1];
|
|
const resources = data[2];
|
|
const unlockedIncidents = data[3];
|
|
const unscheduledIncidents = data[4];
|
|
const bookingChangeIncidents = data[5];
|
|
|
|
const membersMap = {};
|
|
const officesMap = {};
|
|
const resourcesMap = {};
|
|
|
|
members.forEach((member) => membersMap[member.memberId] = member);
|
|
offices.forEach((office) => officesMap[office.officeId] = office);
|
|
resources.forEach((resource) => resourcesMap[resource.resourceId] = resource);
|
|
|
|
const allIncidents = [];
|
|
|
|
unlockedIncidents.forEach((unlockedIncident) => {
|
|
const incidentTypeNumber = unlockedIncident.reservationId ?
|
|
incidentType.UNLOCKED_INCIDENT_RELATED_WITH_RESERVATION : incidentType.UNLOCKED_INCIDENT_STANDALONE;
|
|
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),
|
|
bookingStartRaw: unlockedIncident.bookingStart,
|
|
bookingEndRaw: unlockedIncident.bookingEnd,
|
|
unlockTimestamp: formatTime(unlockedIncident.unlockTimestamp),
|
|
unlockTimestampRaw: unlockedIncident.unlockTimestamp,
|
|
incidentType: incidentTypeNumber,
|
|
incidentLevel: unlockedIncident.incidentLevel,
|
|
incidentPrice: unlockedIncident.incidentLevelPrice,
|
|
});
|
|
});
|
|
|
|
unscheduledIncidents.forEach((unscheduledIncident) => {
|
|
let incidentTypeNumber;
|
|
if (unscheduledIncident.reservationId){
|
|
if (unscheduledIncident.unlockTimestamp && !unscheduledIncident.lockTimestamp){
|
|
incidentTypeNumber = incidentType.UNSCHEDULED_INCIDENT_BEFORE_RESERVATION;
|
|
}else{
|
|
incidentTypeNumber = incidentType.UNSCHEDULED_INCIDENT_AFTER_RESERVATION;
|
|
}
|
|
}else{
|
|
incidentTypeNumber = incidentType.UNSCHEDULED_INCIDENT_STANDALONE;
|
|
}
|
|
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),
|
|
bookingStartRaw: unscheduledIncident.bookingStart,
|
|
bookingEndRaw: unscheduledIncident.bookingEnd,
|
|
unlockTimestamp: formatTime(unscheduledIncident.unlockTimestamp),
|
|
lockTimestamp: formatTime(unscheduledIncident.lockTimestamp),
|
|
unlockTimestampRaw: unscheduledIncident.unlockTimestamp,
|
|
lockTimestampRaw: unscheduledIncident.lockTimestamp,
|
|
incidentType: incidentTypeNumber,
|
|
timeIntervalsToCharge: unscheduledIncident.timeIntervalsToCharge,
|
|
chargePrice: unscheduledIncident.chargePrice,
|
|
totalChargeFee: unscheduledIncident.totalChargeFee,
|
|
});
|
|
});
|
|
|
|
bookingChangeIncidents.forEach((bookingChangeIncident) => {
|
|
const {
|
|
id,
|
|
memberId,
|
|
oldResourceId,
|
|
newResourceId,
|
|
oldBookingStart,
|
|
oldBookingEnd,
|
|
newBookingStart,
|
|
newBookingEnd,
|
|
incidentType,
|
|
chargeFee,
|
|
createdAt,
|
|
} = bookingChangeIncident;
|
|
const memberName = membersMap[memberId].name;
|
|
const oldResource = resourcesMap[oldResourceId];
|
|
const newResource = newResourceId ? resourcesMap[newResourceId] : null;
|
|
const oldResourceName = oldResource.resourceName;
|
|
const newResourceName = newResource ? newResource.resourceName : null;
|
|
const officeId = oldResource.officeId;
|
|
const officeName = officesMap[officeId].officeName;
|
|
allIncidents.push({
|
|
incidentId: id,
|
|
memberId,
|
|
memberName,
|
|
oldResourceName,
|
|
newResourceName,
|
|
officeId,
|
|
officeName,
|
|
oldBookingStart: formatTime(oldBookingStart),
|
|
oldBookingEnd: formatTime(oldBookingEnd),
|
|
newBookingStart: formatTime(newBookingStart),
|
|
newBookingEnd: formatTime(newBookingEnd),
|
|
oldBookingStartRaw: oldBookingStart,
|
|
oldBookingEndRaw: oldBookingEnd,
|
|
newBookingStartRaw: newBookingStart,
|
|
newBookingEndRaw: newBookingEnd,
|
|
incidentType,
|
|
totalChargeFee: chargeFee,
|
|
incidentTimestamp: formatTime(createdAt),
|
|
incidentTimestampRaw: createdAt,
|
|
});
|
|
});
|
|
|
|
resolve(allIncidents);
|
|
})
|
|
.catch((error) => reject(error));
|
|
});
|
|
};
|
|
|
|
const getMemberPracticeSummaryReport = (res) => {
|
|
return new Promise((resolve, reject) => {
|
|
const year = moment.tz(UI_TIMEZONE).year();
|
|
|
|
const asyncJobs = [checkBookingChanges(), getAllBookingsForYear(year), fetchAllMembers()];
|
|
|
|
Promise.all(asyncJobs)
|
|
.then((results) => {
|
|
const allBookings = results[1];
|
|
const allMembers = results[2];
|
|
|
|
const membersMap = {};
|
|
|
|
allMembers.forEach((member) => {
|
|
membersMap[member.memberId] = member;
|
|
});
|
|
|
|
const reportMap = {};
|
|
|
|
const oneMonthObject = {
|
|
totalBookedHours: 0,
|
|
totalChargedHours: 0,
|
|
cancellationPercentage: 0,
|
|
growthRate: 0,
|
|
};
|
|
|
|
const oneMemberObject = [];
|
|
for (let i = 0; i < 12; i++) {
|
|
oneMemberObject.push(Object.assign({}, oneMonthObject));
|
|
}
|
|
|
|
const reservationIdsForAdditionalData = [];
|
|
|
|
allBookings.forEach((booking) => {
|
|
const {reservationId, memberId, start, end, timezone, canceled} = booking.get();
|
|
const startMoment = moment.tz(start, timezone);
|
|
const endMoment = moment.tz(end, timezone);
|
|
|
|
if (startMoment.isValid() && endMoment.isValid()) {
|
|
const bookingMonth = startMoment.month();
|
|
const bookingLength = endMoment.diff(startMoment, 'hours', true);
|
|
|
|
if (!reportMap[memberId]) {
|
|
reportMap[memberId] = JSON.parse(JSON.stringify(oneMemberObject));
|
|
}
|
|
|
|
reportMap[memberId][bookingMonth].totalBookedHours += bookingLength;
|
|
|
|
if (canceled) {
|
|
reservationIdsForAdditionalData.push(reservationId);
|
|
} else {
|
|
reportMap[memberId][bookingMonth].totalChargedHours += bookingLength;
|
|
}
|
|
}
|
|
});
|
|
|
|
getChargedCanceledReservations(reservationIdsForAdditionalData)
|
|
.then((incidents) => {
|
|
incidents.forEach((incident) => {
|
|
const {memberId, oldBookingStart, oldBookingEnd} = incident.get();
|
|
|
|
const startMoment = moment.tz(oldBookingStart, UI_TIMEZONE);
|
|
const endMoment = moment.tz(oldBookingEnd, UI_TIMEZONE);
|
|
|
|
if (startMoment.isValid() && endMoment.isValid()) {
|
|
const bookingMonth = startMoment.month();
|
|
const bookingLength = endMoment.diff(startMoment, 'hours', true);
|
|
|
|
reportMap[memberId][bookingMonth].totalChargedHours += bookingLength;
|
|
}
|
|
|
|
});
|
|
|
|
// Generate report sheet
|
|
|
|
const reportWorkbook = new workbookCreator.Workbook({});
|
|
const reportWorksheet = reportWorkbook.addWorksheet('Sheet 1');
|
|
|
|
const titleStyle = reportWorkbook.createStyle({
|
|
font: {
|
|
size: 18
|
|
}
|
|
});
|
|
const centeredStyle = reportWorkbook.createStyle({
|
|
alignment: {
|
|
horizontal: 'center',
|
|
}
|
|
});
|
|
const headerStyle = reportWorkbook.createStyle({
|
|
font: {
|
|
bold: true,
|
|
},
|
|
fill: {
|
|
type: 'pattern',
|
|
patternType: 'solid',
|
|
fgColor: '48cdd4',
|
|
}
|
|
});
|
|
const solidBlackBorder = {
|
|
left: {
|
|
style: 'thin',
|
|
color: '000000',
|
|
},
|
|
right: {
|
|
style: 'thin',
|
|
color: '000000',
|
|
},
|
|
top: {
|
|
style: 'thin',
|
|
color: '000000',
|
|
},
|
|
bottom: {
|
|
style: 'thin',
|
|
color: '000000',
|
|
}
|
|
};
|
|
const yellowCellStyle = reportWorkbook.createStyle({
|
|
fill: {
|
|
type: 'pattern',
|
|
patternType: 'solid',
|
|
fgColor: 'eaed37',
|
|
},
|
|
border: solidBlackBorder,
|
|
});
|
|
const redCellStyle = reportWorkbook.createStyle({
|
|
fill: {
|
|
type: 'pattern',
|
|
patternType: 'solid',
|
|
fgColor: 'e81717',
|
|
},
|
|
border: solidBlackBorder,
|
|
});
|
|
const greenCellStyle = reportWorkbook.createStyle({
|
|
fill: {
|
|
type: 'pattern',
|
|
patternType: 'solid',
|
|
fgColor: '329932',
|
|
},
|
|
border: solidBlackBorder,
|
|
});
|
|
const decimalPointStyle = reportWorkbook.createStyle({
|
|
numberFormat: '#,##0.00; -#,##0.00; -'
|
|
});
|
|
|
|
reportWorksheet.cell(2, 6, 2, 8, true).string('Member Practice Summary Report').style(titleStyle);
|
|
reportWorksheet.row(2).setHeight(20); // 20 is good height for font 18
|
|
|
|
reportWorksheet.cell(4, 6).string(`Year : ${year}`);
|
|
|
|
reportWorksheet.cell(7, 1).string('Member name');
|
|
reportWorksheet.column(1).setWidth(35);
|
|
|
|
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
for (let i = 0; i < 12; i++) {
|
|
reportWorksheet.cell(6, 3 + i * 5, 6, 6 + i * 5, true).string(monthNames[i]).style(centeredStyle);
|
|
|
|
reportWorksheet.cell(7, 3 + i * 5).string('Total Booked [h]');
|
|
reportWorksheet.cell(7, 4 + i * 5).string('Total Charged [h]');
|
|
reportWorksheet.cell(7, 5 + i * 5).string('Cancellation [%]');
|
|
reportWorksheet.cell(7, 6 + i * 5).string('Growth [%]');
|
|
|
|
reportWorksheet.column(3 + i * 5).setWidth(20);
|
|
reportWorksheet.column(4 + i * 5).setWidth(20);
|
|
reportWorksheet.column(5 + i * 5).setWidth(20);
|
|
reportWorksheet.column(6 + i * 5).setWidth(20);
|
|
}
|
|
|
|
reportWorksheet.cell(7, 1, 7, 61).style(headerStyle);
|
|
|
|
const memberIdsListFromReportMap = Object.keys(reportMap);
|
|
|
|
const activeMemberIdsList = [];
|
|
const inactiveMemberIdsList = [];
|
|
|
|
memberIdsListFromReportMap.forEach((memberId) => {
|
|
if (membersMap[memberId].active){
|
|
activeMemberIdsList.push(memberId);
|
|
}else{
|
|
inactiveMemberIdsList.push(memberId);
|
|
}
|
|
});
|
|
|
|
const sortMemberIdsListByName = (memberId1, memberId2) => {
|
|
if (membersMap[memberId1].name > membersMap[memberId2].name){
|
|
return 1;
|
|
}else{
|
|
return -1;
|
|
}
|
|
};
|
|
activeMemberIdsList.sort(sortMemberIdsListByName);
|
|
inactiveMemberIdsList.sort(sortMemberIdsListByName);
|
|
|
|
const populateReportForMember = (startRow, memberId, index) => {
|
|
|
|
const memberName = membersMap[memberId] ? membersMap[memberId].name : 'Unknown member';
|
|
reportWorksheet.cell(startRow + index, 1).string(memberName);
|
|
|
|
const memberReport = reportMap[memberId];
|
|
for (let i = 0; i< 12; i++){
|
|
const totalBookedHours = memberReport[i].totalBookedHours;
|
|
const totalChargedHoursCurrentMonth = memberReport[i].totalChargedHours;
|
|
let totalChargedHoursPreviousMonth = totalChargedHoursCurrentMonth;
|
|
if (i > 0){
|
|
totalChargedHoursPreviousMonth = memberReport[i-1].totalChargedHours;
|
|
}
|
|
|
|
let cancellationPercentage = ((totalBookedHours - totalChargedHoursCurrentMonth) / totalBookedHours)*100;
|
|
let growthRate = ((totalChargedHoursCurrentMonth - totalChargedHoursPreviousMonth) / totalChargedHoursPreviousMonth)*100;
|
|
if (isNaN(cancellationPercentage) || !cancellationPercentage){
|
|
cancellationPercentage = 0;
|
|
}
|
|
if (isNaN(growthRate) || growthRate > 100){
|
|
growthRate = 0;
|
|
}
|
|
|
|
const totalBookedHoursCell = reportWorksheet.cell(startRow + index, 3 + i*5);
|
|
const totalChargedHoursCell = reportWorksheet.cell(startRow + index, 4 + i*5);
|
|
const cancellationRateCell = reportWorksheet.cell(startRow + index, 5 + i*5);
|
|
const growthRateCell = reportWorksheet.cell(startRow + index, 6 + i*5);
|
|
|
|
totalBookedHoursCell.number(totalBookedHours).style(decimalPointStyle);
|
|
totalChargedHoursCell.number(totalChargedHoursCurrentMonth).style(decimalPointStyle);
|
|
cancellationRateCell.number(cancellationPercentage).style(decimalPointStyle);
|
|
growthRateCell.number(growthRate).style(decimalPointStyle);
|
|
|
|
if (cancellationPercentage > 30){
|
|
cancellationRateCell.style(yellowCellStyle);
|
|
}
|
|
|
|
if (growthRate > 30){
|
|
growthRateCell.style(greenCellStyle);
|
|
}
|
|
if (growthRate < -30){
|
|
growthRateCell.style(redCellStyle);
|
|
}
|
|
}
|
|
};
|
|
|
|
const startRowForActiveMembers = 8;
|
|
activeMemberIdsList.forEach((memberId, index) => populateReportForMember(startRowForActiveMembers, memberId, index));
|
|
|
|
const inactiveMembersHeaderRow = startRowForActiveMembers + activeMemberIdsList.length + 1;
|
|
const startRowForInactiveMembers = inactiveMembersHeaderRow + 2;
|
|
reportWorksheet.cell(inactiveMembersHeaderRow, 1).string('Deactivated members').style(headerStyle);
|
|
|
|
inactiveMemberIdsList.forEach((memberId, index) => populateReportForMember(startRowForInactiveMembers, memberId, index));
|
|
|
|
reportWorksheet.cell(1, 1).string('Generated at : ');
|
|
reportWorksheet.cell(2, 1).string(moment.tz(UI_TIMEZONE).format('MMM DD, YYYY HH:mm a'));
|
|
|
|
const reportName = `${moment.tz(UI_TIMEZONE).format('DDMMYYYYHHmmss')}.xlsx`;
|
|
const reportPath = `report_sheets/${reportName}`;
|
|
reportWorkbook.write(reportPath, (error) => {
|
|
if (error){
|
|
reject(error);
|
|
}
|
|
resolve({reportPath});
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
console.log('Error : ', error);
|
|
resolve({
|
|
report: reportMap,
|
|
error: integrationServiceErrors.ERRORS_IN_MEMBER_PRACTICE_SUMMARY_REPORT
|
|
});
|
|
});
|
|
})
|
|
.catch((error) => reject(error));
|
|
});
|
|
};
|
|
|
|
module.exports = {
|
|
getAllIncidents,
|
|
getMemberPracticeSummaryReport,
|
|
};
|