'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, };