generate practice summary report; send report to frontend
This commit is contained in:
12890
client/package-lock.json
generated
Normal file
12890
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Container, Button } from 'semantic-ui-react';
|
||||
import { Container, Button, Loader } from 'semantic-ui-react';
|
||||
|
||||
import MainMenu from '../../components/MainMenu';
|
||||
|
||||
@@ -8,20 +8,24 @@ import { fetchMemberPracticeSummaryReport } from '../../store/actions';
|
||||
|
||||
class MemberPracticeSummaryReport extends Component {
|
||||
render () {
|
||||
const { fetchMemberPracticeSummaryReport } = this.props;
|
||||
const { fetchMemberPracticeSummaryReport, pendingReport } = this.props;
|
||||
return (
|
||||
<Container>
|
||||
<MainMenu/>
|
||||
<h3>Member Practice Summary Report</h3>
|
||||
<hr/>
|
||||
<br/>
|
||||
<Button onClick={fetchMemberPracticeSummaryReport}>Generate Report</Button>
|
||||
<Loader active={pendingReport} />
|
||||
<Button disabled={pendingReport} onClick={fetchMemberPracticeSummaryReport}>Generate Report</Button>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => ({});
|
||||
const mapStateToProps = (state) => ({
|
||||
pendingReport: state.memberPracticeSummaryReport.pending,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
fetchMemberPracticeSummaryReport: () => fetchMemberPracticeSummaryReport(dispatch),
|
||||
});
|
||||
|
||||
@@ -115,9 +115,11 @@ export const checkProcessing = (dispatch) => {
|
||||
|
||||
export const fetchMemberPracticeSummaryReport = (dispatch) => {
|
||||
dispatch({type: FETCH_MEMBER_PRACTICE_SUMMARY_REPORT_PENDING});
|
||||
API.get('integration/report/practiceSummary')
|
||||
API.get('integration/report/practiceSummary', {
|
||||
responseType: 'blob',
|
||||
})
|
||||
.then(response => {
|
||||
dispatch({type: FETCH_MEMBER_PRACTICE_SUMMARY_REPORT_SUCCESS, payload: response.data});
|
||||
dispatch({type: FETCH_MEMBER_PRACTICE_SUMMARY_REPORT_SUCCESS, payload: response});
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch({type: FETCH_MEMBER_PRACTICE_SUMMARY_REPORT_FAILED, payload: error.response});
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
FETCH_MEMBER_PRACTICE_SUMMARY_REPORT_PENDING,
|
||||
FETCH_MEMBER_PRACTICE_SUMMARY_REPORT_SUCCESS,
|
||||
FETCH_MEMBER_PRACTICE_SUMMARY_REPORT_FAILED,
|
||||
} from '../constants';
|
||||
|
||||
const initialState = {
|
||||
pending: false,
|
||||
result: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export const memberPracticeSummaryReport = (state, action) => {
|
||||
state = state || initialState;
|
||||
action = action || {};
|
||||
|
||||
switch(action.type){
|
||||
case FETCH_MEMBER_PRACTICE_SUMMARY_REPORT_PENDING:
|
||||
return Object.assign({}, state, {
|
||||
pending: true,
|
||||
error: null,
|
||||
});
|
||||
case FETCH_MEMBER_PRACTICE_SUMMARY_REPORT_SUCCESS:
|
||||
const url = window.URL.createObjectURL(new Blob([action.payload.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', 'Member Practice Summary Report.xlsx');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
return Object.assign({}, state, {
|
||||
pending: false,
|
||||
result: null,
|
||||
error: null,
|
||||
});
|
||||
case FETCH_MEMBER_PRACTICE_SUMMARY_REPORT_FAILED:
|
||||
return Object.assign({}, state, {
|
||||
pending: false,
|
||||
result: {},
|
||||
error: action.payload,
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -8,6 +8,7 @@ import { membersList } from './membersListReducer';
|
||||
import { memberIncidents} from './memberIncidentsReducer';
|
||||
import { addFeesStatus } from './addFeesToOrdReducer';
|
||||
import { checkProcessing } from './checkProcessingReducer';
|
||||
import { memberPracticeSummaryReport} from './fetchMemberPracticeSummaryReportReducer';
|
||||
|
||||
export const rootReducer = combineReducers({
|
||||
doorLockData,
|
||||
@@ -18,5 +19,6 @@ export const rootReducer = combineReducers({
|
||||
memberIncidents,
|
||||
addFeesStatus,
|
||||
checkProcessing,
|
||||
memberPracticeSummaryReport,
|
||||
});
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ const integrationServiceErrors = {
|
||||
FAILED_TO_SAVE_DATA_GENERIC: 'Failed to save data',
|
||||
INVALID_DATE_RANGE: 'Dates in date range are invalid',
|
||||
FAILED_TO_GENERATE_MEMBER_PRACTICE_SUMMARY: 'Failed to generate Member Practice Summary',
|
||||
ERRORS_IN_MEMBER_PRACTICE_SUMMARY_REPORT: 'Member Practice Summary Report is generated but there were some errors and report may be incomplete',
|
||||
};
|
||||
|
||||
const incidentType = {
|
||||
|
||||
@@ -128,10 +128,13 @@ const checkProcessingStatus = (req, res) => {
|
||||
|
||||
const getPracticeSummaryReport = (req, res) => {
|
||||
getMemberPracticeSummaryReport()
|
||||
.then(() => {
|
||||
res.send();
|
||||
.then((result) => {
|
||||
const pathToDownloadFile = `${__dirname}/../${result.reportPath}`;
|
||||
res.download(pathToDownloadFile);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('Error - Member Practice Summary Report');
|
||||
console.log(error);
|
||||
res.status(500).send(error);
|
||||
});
|
||||
};
|
||||
|
||||
1
report_sheets/.gitignore
vendored
Normal file
1
report_sheets/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.xlsx
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const moment = require('moment-timezone');
|
||||
const db = require('../../models/index');
|
||||
const Op = require('sequelize').Op;
|
||||
|
||||
const { UI_TIMEZONE, BOOKING_CHANGE_PERCENTAGE_CHARGE, ALLOWED_BOOKING_CANCELLATION_TIME, incidentType } = require('../../constants/constants');
|
||||
|
||||
@@ -128,6 +129,20 @@ const chargeBookingChanges = (changes) => {
|
||||
});
|
||||
};
|
||||
|
||||
const getChargedCanceledReservations = (reservationIds) => {
|
||||
const filters = {
|
||||
reservationId: {
|
||||
[Op.in]: reservationIds,
|
||||
},
|
||||
incidentType: incidentType.BOOKING_CANCELED_LATE,
|
||||
};
|
||||
|
||||
const attributes = ['memberId', 'oldBookingStart', 'oldBookingEnd'];
|
||||
|
||||
return db.bookingChangeIncident.findAll({attributes, where: filters});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
chargeBookingChanges,
|
||||
getChargedCanceledReservations,
|
||||
};
|
||||
|
||||
@@ -48,6 +48,41 @@ const getActiveBookingsForMembersInDateRange = (dateRange, memberIds) => {
|
||||
});
|
||||
};
|
||||
|
||||
const getAllBookingsForYear = (year) => {
|
||||
const startDate = moment.tz(year, 'YYYY', UI_TIMEZONE).startOf('year');
|
||||
const endDate = moment.tz(year, 'YYYY', UI_TIMEZONE).endOf('year');
|
||||
|
||||
const attributes = [
|
||||
'id',
|
||||
'reservationId',
|
||||
'memberId',
|
||||
'officeId',
|
||||
'resourceId',
|
||||
'start',
|
||||
'end',
|
||||
'timezone',
|
||||
'canceled',
|
||||
'hourlyRate'
|
||||
];
|
||||
|
||||
const filters = {};
|
||||
|
||||
if (startDate && endDate) {
|
||||
filters.start = {
|
||||
[Op.gte]: startDate.toISOString()
|
||||
};
|
||||
filters.end = {
|
||||
[Op.lte]: endDate.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
return db.bookingReservation.findAll({
|
||||
attributes,
|
||||
where: filters,
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getActiveBookingsForMembersInDateRange,
|
||||
getAllBookingsForYear,
|
||||
};
|
||||
|
||||
@@ -5,10 +5,15 @@ 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'];
|
||||
@@ -313,9 +318,274 @@ const getAllIncidents = (dateRange, memberIds) => {
|
||||
});
|
||||
};
|
||||
|
||||
const getMemberPracticeSummaryReport = () => {
|
||||
const getMemberPracticeSummaryReport = (res) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve();
|
||||
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));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ const fetchAllMembers = () => {
|
||||
name: member.name,
|
||||
memberId: member['_id'],
|
||||
teamId: member.team,
|
||||
active: member.status === 'active',
|
||||
});
|
||||
});
|
||||
cleanedResult.sort((member1, member2) => (member1.name > member2.name) ? 1 : -1 );
|
||||
|
||||
Reference in New Issue
Block a user