Files
old-psihologija/services/integration/reports.js

648 lines
28 KiB
JavaScript

'use strict';
const moment = require('moment-timezone');
const db = require('../../models/index');
const Op = require('sequelize').Op;
const workbookCreator = require('excel4node');
const { incidentType, UI_TIMEZONE, DEFAULT_DATE_FORMAT, integrationServiceErrors } = require('../../constants/constants');
const { getAllBookingsForMembersInDateRange } = 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 = {
deleted: false
};
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 = {
deleted: false
};
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 = {
deleted: false,
};
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;
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,
resourceName,
officeId,
officeName,
officeSlug,
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;
}
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,
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) || '-',
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,
deleted,
createdAt,
} = bookingChangeIncident;
const memberName = membersMap[memberId] ? membersMap[memberId].name : 'Unknown member';
const oldResource = resourcesMap[oldResourceId];
const newResource = newResourceId ? resourcesMap[newResourceId] : null;
const oldResourceName = oldResource ? oldResource.resourceName : 'Unknown room';
const newResourceName = newResource ? newResource.resourceName : null;
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,
memberName,
oldResourceName,
newResourceName,
officeId,
officeName,
officeSlug,
oldBookingStart: formatTime(oldBookingStart) || '-',
oldBookingEnd: formatTime(oldBookingEnd) || '-',
newBookingStart: formatTime(newBookingStart) || '-',
newBookingEnd: formatTime(newBookingEnd) || '-',
oldBookingStartRaw: oldBookingStart,
oldBookingEndRaw: oldBookingEnd,
newBookingStartRaw: newBookingStart,
newBookingEndRaw: newBookingEnd,
incidentType,
totalChargeFee: chargeFee,
deleted,
incidentTimestamp: formatTime(createdAt) || '-',
incidentTimestampRaw: createdAt,
});
});
resolve(allIncidents);
})
.catch((error) => reject(error));
});
};
const getMemberPracticeSummaryReport = (year) => {
return new Promise((resolve, reject) => {
const startDate = moment.tz(year, 'YYYY', UI_TIMEZONE).startOf('year').format(DEFAULT_DATE_FORMAT);
const endDate = moment.tz(year, 'YYYY', UI_TIMEZONE).endOf('year').format(DEFAULT_DATE_FORMAT);
const dateRange = {
startDate,
endDate,
};
const asyncJobs = [getAllBookingsForMembersInDateRange(dateRange), fetchAllMembers()];
Promise.all(asyncJobs)
.then((results) => {
const allBookings = results[0];
const allMembers = results[1];
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) => {
console.log('Charged canceled reservations ...');
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 (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{
console.log('[Get Member Practice Summary Report] memberId is wrong : ', memberId);
}
});
const sortMemberIdsListByName = (memberId1, memberId2) => {
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;
}
};
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,
};