diff --git a/client/package.json b/client/package.json index 26300b5..4aa04dc 100644 --- a/client/package.json +++ b/client/package.json @@ -5,6 +5,7 @@ "dependencies": { "axios": "^0.18.0", "fuse.js": "^3.4.5", + "moment": "^2.24.0", "react": "^16.8.6", "react-dom": "^16.8.6", "react-redux": "^7.0.3", diff --git a/client/src/components/DateRangePicker/index.js b/client/src/components/DateRangePicker/index.js new file mode 100644 index 0000000..74d23f7 --- /dev/null +++ b/client/src/components/DateRangePicker/index.js @@ -0,0 +1,128 @@ +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 buttonLabel = this.props.buttonLabel || 'Save'; + + const startDateValue = startDate.format(defaultDateFormat); + const endDateValue = endDate.format(defaultDateFormat); + + return ( +
+ + + + + + + + + + + + { error && + + + + Wrong date +

{startDateLabel} has to be before {endDateLabel}

+
+
+
+ } + + + {buttonLabel} + + +
+
+ ); + } +} + +export default DateRangePicker; diff --git a/client/src/components/MemberIncidentsTable/index.js b/client/src/components/MemberIncidentsTable/index.js new file mode 100644 index 0000000..2e6b44f --- /dev/null +++ b/client/src/components/MemberIncidentsTable/index.js @@ -0,0 +1,94 @@ +import React from 'react'; +import { Loader } from 'semantic-ui-react'; +import ReactTable from 'react-table'; +import 'react-table/react-table.css'; + +import {incidentsReportHeaderTitles} from '../../constants/menuItems'; +import { + incidentDescriptions, + incidentLevelDescriptions, + UNLOCKED_INCIDENT, + UNSCHEDULED_INCIDENT +} from '../../constants/enums'; + + +const MemberIncidentsTable = props => { + const { loading, title } = props; + const incidents = props.incidents ? props.incidents : []; + + const columns = []; + if (incidents && incidents.length > 0){ + const incidentHeaders = Object.keys(incidentsReportHeaderTitles); + + incidentHeaders.forEach((header) => { + const columnTitle = incidentsReportHeaderTitles[header]; + + if (columnTitle){ + const columnAlignments = { + left: 'left', + right: 'right', + }; + let columnContentsAlignment = columnAlignments.left; + + columns.push({ + Header: incidentsReportHeaderTitles[header], + accessor: header, + Cell: props => { + let cellValue; + + switch (props.column.id) { + 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: + cellValue = `${incidentLevelDescriptions[incidentLevel]}`; + break; + case UNSCHEDULED_INCIDENT: + cellValue = `${timeIntervalsToCharge} x 5 min`; + break; + default: + cellValue = ''; + break; + } + break; + case 'totalChargeFee': + const totalFee = props.value ? props.value : props.row['_original'].incidentPrice; + const totalFeeFormatted = parseFloat(totalFee).toFixed(2); + cellValue = `$ ${totalFeeFormatted}`; + columnContentsAlignment = columnAlignments.right; + break; + default: + cellValue = props.value; + } + + return
{cellValue}
+ } + }); + } + }); + } + + return ( +
+

{title}

+ + { + !loading && incidents && + + } +
+ ); +}; + +export default MemberIncidentsTable; diff --git a/client/src/constants/constants.js b/client/src/constants/constants.js new file mode 100644 index 0000000..ed5633b --- /dev/null +++ b/client/src/constants/constants.js @@ -0,0 +1 @@ +export const defaultDateFormat = 'YYYY-MM-DD'; diff --git a/client/src/constants/menuItems.js b/client/src/constants/menuItems.js index 8a910e3..e2ab184 100644 --- a/client/src/constants/menuItems.js +++ b/client/src/constants/menuItems.js @@ -1,6 +1,7 @@ import UploadDLockData from '../scenes/UploadDLockData'; import Home from '../scenes/Home'; import IncidentsReport from '../scenes/IncidentsReport'; +import PracticeSummaryReport from '../scenes/PracticeSummaryReport'; export const mainMenuItems = [ { @@ -9,6 +10,12 @@ export const mainMenuItems = [ url: '/', component: Home, }, + { + id: 'practiceSummaryReport', + title: 'Practice Summary Report', + url: '/practice-summary-report', + component: PracticeSummaryReport, + }, { id: 'report', title: 'Incidents Report', diff --git a/client/src/scenes/IncidentsReport/index.js b/client/src/scenes/IncidentsReport/index.js index 09a76cb..6501d47 100644 --- a/client/src/scenes/IncidentsReport/index.js +++ b/client/src/scenes/IncidentsReport/index.js @@ -1,96 +1,30 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; -import { Container, Loader } from 'semantic-ui-react'; -import ReactTable from 'react-table'; -import 'react-table/react-table.css'; +import { Container } from 'semantic-ui-react'; import MainMenu from '../../components/MainMenu'; +import DateRangePicker from '../../components/DateRangePicker'; +import MemberIncidentsTable from '../../components/MemberIncidentsTable'; + import { fetchIncidents } from '../../store/actions'; -import { incidentsReportHeaderTitles } from '../../constants/menuItems'; -import { incidentDescriptions, incidentLevelDescriptions, UNSCHEDULED_INCIDENT, UNLOCKED_INCIDENT } from '../../constants/enums'; class IncidentsReport extends Component { - - componentDidMount() { + onDatesUpdate(dateRange) { const { fetchIncidents } = this.props; - fetchIncidents(); + fetchIncidents(dateRange); } render () { const { pendingIncidents, incidents } = this.props; - const columns = []; - if (incidents && incidents.length > 0){ - const incidentHeaders = Object.keys(incidentsReportHeaderTitles); - - incidentHeaders.forEach((header) => { - const columnTitle = incidentsReportHeaderTitles[header]; - - if (columnTitle){ - const columnAlignments = { - left: 'left', - right: 'right', - }; - let columnContentsAlignment = columnAlignments.left; - - columns.push({ - Header: incidentsReportHeaderTitles[header], - accessor: header, - Cell: props => { - let cellValue; - - switch (props.column.id) { - 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: - cellValue = `${incidentLevelDescriptions[incidentLevel]}`; - break; - case UNSCHEDULED_INCIDENT: - cellValue = `${timeIntervalsToCharge} x 5 min`; - break; - } - break; - - case 'totalChargeFee': - const totalFee = props.value ? props.value : props.row['_original'].incidentPrice; - const totalFeeFormatted = parseFloat(totalFee).toFixed(2); - cellValue = `$ ${totalFeeFormatted}`; - columnContentsAlignment = columnAlignments.right; - break; - - default: - cellValue = props.value; - } - - return
{cellValue}
- } - }); - } - }); - } - return (

Incidents Report


- - { - !pendingIncidents && incidents && - - } + +
+
); } @@ -102,7 +36,7 @@ const mapStateToProps = (state) => ({ }); const mapDispatchToProps = (dispatch) => ({ - fetchIncidents: () => fetchIncidents(dispatch), + fetchIncidents: (dateRange) => fetchIncidents(dispatch, dateRange), }); export default connect(mapStateToProps, mapDispatchToProps)(IncidentsReport); diff --git a/client/src/scenes/PracticeSummaryReport/components/MemberSelector.js b/client/src/scenes/PracticeSummaryReport/components/MemberSelector.js new file mode 100644 index 0000000..2c439b3 --- /dev/null +++ b/client/src/scenes/PracticeSummaryReport/components/MemberSelector.js @@ -0,0 +1,58 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { Dropdown, Form } from 'semantic-ui-react'; + +import { fetchMembersList } from '../../../store/actions'; + +class MemberSelector extends Component { + componentDidMount() { + const { fetchMembersList } = this.props; + + fetchMembersList(); + } + + onMemberSelectionChange(event, data){ + const { onMemberSelect } = this.props; + + const { value } = data; + + if (onMemberSelect && value){ + onMemberSelect(value); + } + } + + render(){ + const { members } = this.props; + + const dropdownOptions = members && Array.isArray(members) ? members.map(member => ({ + key: member.memberId, + value: member.memberId, + text: member.name + }) + ) : null; + + return ( +
+ + + + ); + } +} + +const mapStateToProps = (state) => ({ + members: state.membersList.result, +}); + +const mapDispatchToProps = (dispatch) => ({ + fetchMembersList: () => fetchMembersList(dispatch), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(MemberSelector); diff --git a/client/src/scenes/PracticeSummaryReport/components/MemberSummary.js b/client/src/scenes/PracticeSummaryReport/components/MemberSummary.js new file mode 100644 index 0000000..3be23d6 --- /dev/null +++ b/client/src/scenes/PracticeSummaryReport/components/MemberSummary.js @@ -0,0 +1,9 @@ +import React, { Component } from 'react'; + +class MemberSummary extends Component { + render() { + return (

Member Summary

); + } +} + +export default MemberSummary; diff --git a/client/src/scenes/PracticeSummaryReport/index.js b/client/src/scenes/PracticeSummaryReport/index.js new file mode 100644 index 0000000..baa1e42 --- /dev/null +++ b/client/src/scenes/PracticeSummaryReport/index.js @@ -0,0 +1,87 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import {Container, Grid} from 'semantic-ui-react'; + +import MainMenu from '../../components/MainMenu'; +import DateRangePicker from '../../components/DateRangePicker'; +import MemberSelector from './components/MemberSelector'; +import MemberSummary from './components/MemberSummary'; +import MemberIncidentsTable from '../../components/MemberIncidentsTable'; + +import { fetchMemberIncidents } from '../../store/actions'; + +class PracticeSummaryReport extends Component { + constructor(props){ + super(props); + + this.state = { + dateRange: null, + memberId: null, + }; + } + + onDateRangeUpdate(dateRange){ + this.fetchIncidents(dateRange, this.state.memberId); + this.setState({dateRange}); + } + + onMemberSelectionUpdate(memberId){ + this.fetchIncidents(this.state.dateRange, memberId); + this.setState({memberId}); + } + + fetchIncidents(dateRange, memberId){ + const { fetchMemberIncidents } = this.props; + + if (dateRange && dateRange.startDate && dateRange.endDate && memberId){ + fetchMemberIncidents(memberId, dateRange); + } + } + + render () { + const { memberIncidents, loading } = this.props; + + return ( + + +

Practice Summary Report

+
+ + + + + + + + + + + + + + + + + + + + +
+ ); + } +} + +const mapStateToProps = (state) => ({ + memberIncidents: state.memberIncidents.result, + loading: state.memberIncidents.pending, +}); + +const mapDispatchToProps = (dispatch) => ({ + fetchMemberIncidents: (memberId, dateRange) => fetchMemberIncidents(dispatch, memberId, dateRange), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(PracticeSummaryReport); diff --git a/client/src/store/actions/integrationActions.js b/client/src/store/actions/integrationActions.js index 599573f..5e9a127 100644 --- a/client/src/store/actions/integrationActions.js +++ b/client/src/store/actions/integrationActions.js @@ -8,6 +8,12 @@ import { FETCH_INCIDENTS_PENDING, FETCH_INCIDENTS_SUCCESS, FETCH_INCIDENTS_FAILED, + FETCH_MEMBERS_PENDING, + FETCH_MEMBERS_SUCCESS, + FETCH_MEMBERS_FAILED, + FETCH_MEMBER_INCIDENTS_PENDING, + FETCH_MEMBER_INCIDENTS_SUCCESS, + FETCH_MEMBER_INCIDENTS_FAILED, } from '../constants'; import API from '../../utilities/api'; @@ -36,9 +42,11 @@ export const addNewMapping = (dispatch, mapping) => { }); }; -export const fetchIncidents = (dispatch) => { +export const fetchIncidents = (dispatch, dateRange) => { + const { startDate, endDate } = dateRange; + dispatch({type: FETCH_INCIDENTS_PENDING}); - API.get('integration/report/allIncidents') + API.get(`integration/report/allIncidents/${startDate}/${endDate}`) .then(response => { dispatch({type: FETCH_INCIDENTS_SUCCESS, payload: response.data}); }) @@ -46,3 +54,27 @@ export const fetchIncidents = (dispatch) => { dispatch({type: FETCH_INCIDENTS_FAILED, payload: error.response}); }); }; + +export const fetchMembersList = (dispatch) => { + dispatch({type: FETCH_MEMBERS_PENDING}); + API.get('officeRnD/membersList') + .then(response => { + dispatch({type: FETCH_MEMBERS_SUCCESS, payload: response.data}); + }) + .catch(error => { + dispatch({type: FETCH_MEMBERS_FAILED, payload: error.response}); + }); +}; + +export const fetchMemberIncidents = (dispatch, memberId, dateRange) => { + const { startDate, endDate } = dateRange; + + dispatch({type: FETCH_MEMBER_INCIDENTS_PENDING}); + API.get(`integration/report/member/${memberId}/${startDate}/${endDate}`) + .then(response => { + dispatch({type: FETCH_MEMBER_INCIDENTS_SUCCESS, payload: response.data}); + }) + .catch(error => { + dispatch({type: FETCH_MEMBER_INCIDENTS_FAILED, payload: error.response}); + }); +}; diff --git a/client/src/store/constants.js b/client/src/store/constants.js index 9b20bbc..6dd4fd8 100644 --- a/client/src/store/constants.js +++ b/client/src/store/constants.js @@ -13,3 +13,11 @@ export const ADD_NEW_MAPPING_FAILED = 'ADD_NEW_MAPPING_FAILED'; export const FETCH_INCIDENTS_PENDING = 'FETCH_INCIDENTS_PENDING'; export const FETCH_INCIDENTS_SUCCESS = 'FETCH_INCIDENTS_SUCCESS'; export const FETCH_INCIDENTS_FAILED = 'FETCH_INCIDENTS_FAILED'; + +export const FETCH_MEMBERS_PENDING = 'FETCH_MEMBERS_PENDING'; +export const FETCH_MEMBERS_SUCCESS = 'FETCH_MEMBERS_SUCCESS'; +export const FETCH_MEMBERS_FAILED = 'FETCH_MEMBERS_FAILED'; + +export const FETCH_MEMBER_INCIDENTS_PENDING = 'FETCH_MEMBER_INCIDENTS_PENDING'; +export const FETCH_MEMBER_INCIDENTS_SUCCESS = 'FETCH_MEMBER_INCIDENTS_SUCCESS'; +export const FETCH_MEMBER_INCIDENTS_FAILED = 'FETCH_MEMBER_INCIDENTS_FAILED'; diff --git a/client/src/store/reducers/index.js b/client/src/store/reducers/index.js index 3c060be..869300c 100644 --- a/client/src/store/reducers/index.js +++ b/client/src/store/reducers/index.js @@ -4,11 +4,15 @@ import { doorLockData} from './doorLockReducers'; import { mappingsData } from './mappingsReducer'; import { addMapping } from './addMappingReducer'; import { incidentsReport } from './incidentsReportReducer'; +import { membersList } from './membersListReducer'; +import { memberIncidents} from './memberIncidentsReducer'; export const rootReducer = combineReducers({ doorLockData, mappingsData, addMapping, incidentsReport, + membersList, + memberIncidents, }); diff --git a/client/src/store/reducers/memberIncidentsReducer.js b/client/src/store/reducers/memberIncidentsReducer.js new file mode 100644 index 0000000..04cef0f --- /dev/null +++ b/client/src/store/reducers/memberIncidentsReducer.js @@ -0,0 +1,38 @@ +import { + FETCH_MEMBER_INCIDENTS_PENDING, + FETCH_MEMBER_INCIDENTS_SUCCESS, + FETCH_MEMBER_INCIDENTS_FAILED, +} from '../constants'; + +const initialState = { + pending: false, + result: null, + error: null, +}; + +export const memberIncidents = (state, action) => { + state = state || initialState; + action = action || {}; + + switch(action.type){ + case FETCH_MEMBER_INCIDENTS_PENDING: + return Object.assign({}, state, { + pending: true, + error: null, + }); + case FETCH_MEMBER_INCIDENTS_SUCCESS: + return Object.assign({}, state, { + pending: false, + result: action.payload, + error: null, + }); + case FETCH_MEMBER_INCIDENTS_FAILED: + return Object.assign({}, state, { + pending: false, + result: {}, + error: action.payload, + }); + default: + return state; + } +}; diff --git a/client/src/store/reducers/membersListReducer.js b/client/src/store/reducers/membersListReducer.js new file mode 100644 index 0000000..bf5c1f6 --- /dev/null +++ b/client/src/store/reducers/membersListReducer.js @@ -0,0 +1,38 @@ +import { + FETCH_MEMBERS_PENDING, + FETCH_MEMBERS_SUCCESS, + FETCH_MEMBERS_FAILED, +} from '../constants'; + +const initialState = { + pending: false, + result: null, + error: null, +}; + +export const membersList = (state, action) => { + state = state || initialState; + action = action || {}; + + switch(action.type){ + case FETCH_MEMBERS_PENDING: + return Object.assign({}, state, { + pending: true, + error: null, + }); + case FETCH_MEMBERS_SUCCESS: + return Object.assign({}, state, { + pending: false, + result: action.payload, + error: null, + }); + case FETCH_MEMBERS_FAILED: + return Object.assign({}, state, { + pending: false, + result: {}, + error: action.payload, + }); + default: + return state; + } +}; diff --git a/client/yarn.lock b/client/yarn.lock index dd8a44d..caa1ce2 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -5478,6 +5478,10 @@ mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@ dependencies: minimist "0.0.8" +moment@^2.24.0: + version "2.24.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" + move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" diff --git a/constants/constants.js b/constants/constants.js index 1650930..812c38a 100644 --- a/constants/constants.js +++ b/constants/constants.js @@ -56,6 +56,7 @@ const integrationServiceErrors = { FAILED_TO_SAVE_BOOKINGS: 'Failed to save booking reservations', FAILED_TO_SAVE_DOOR_LOCK_ENTRIES: 'Failed to save door lock entries', FAILED_TO_SAVE_DATA_GENERIC: 'Failed to save data', + INVALID_DATE_RANGE: 'Dates in date range are invalid', }; const incidentType = { @@ -64,6 +65,10 @@ const incidentType = { UNSCHEDULED_INCIDENT: 3, }; +const UI_TIMEZONE = process.env.UI_TIMEZONE || 'America/Los_Angeles'; + +const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD'; + module.exports = { VALID_CSV_HEADERS, USER_ENTRY_EVENT, @@ -75,4 +80,6 @@ module.exports = { unlockedIncidentLevelsPrices, integrationServiceErrors, incidentType, + UI_TIMEZONE, + DEFAULT_DATE_FORMAT, }; diff --git a/controllers/integration.js b/controllers/integration.js index ddaf1cf..fd2cde0 100644 --- a/controllers/integration.js +++ b/controllers/integration.js @@ -33,7 +33,29 @@ const addNewMapping = (req, res) => { }; const getAllIncidents = (req, res) => { - getAllDoorLockIncidents() + const dateRange = { + startDate: req.params.startDate, + endDate: req.params.endDate, + }; + + getAllDoorLockIncidents(dateRange) + .then((incidents) => { + res.send(incidents); + }) + .catch((error) => { + console.log(error); + res.send([]); + }); +}; + +const getMemberIncidents = (req, res) => { + const memberId = req.params.memberId; + const dateRange = { + startDate: req.params.startDate, + endDate: req.params.endDate, + }; + + getAllDoorLockIncidents(dateRange, memberId) .then((incidents) => { res.send(incidents); }) @@ -57,4 +79,5 @@ module.exports = { getAllIncidents, getUnscheduledIncidents, getUnlockedIncidents, + getMemberIncidents, }; diff --git a/controllers/officeRnD.js b/controllers/officeRnD.js new file mode 100644 index 0000000..a2a9a22 --- /dev/null +++ b/controllers/officeRnD.js @@ -0,0 +1,18 @@ +'use strict'; + +const { fetchAllMembers } = require('../services/officeRnD/members'); + +const fetchMembersList = (req, res) => { + fetchAllMembers() + .then((members) => { + res.send(members); + }) + .catch((error) => { + console.log(error); + res.send([]); + }); +}; + +module.exports = { + fetchMembersList, +}; diff --git a/routes/index.js b/routes/index.js index c1b82eb..abe717c 100644 --- a/routes/index.js +++ b/routes/index.js @@ -2,7 +2,9 @@ const { apiStatusCheck } = require('../controllers/apiStatusCheck'); const { uploadDoorLockData } = require('../controllers/doorLock'); -const { getKnownOfficeResourceMappings, addNewMapping, getAllIncidents,getUnscheduledIncidents, getUnlockedIncidents } = require('../controllers/integration'); +const { getKnownOfficeResourceMappings, addNewMapping, getAllIncidents, getMemberIncidents,getUnscheduledIncidents, getUnlockedIncidents } = require('../controllers/integration'); +const { fetchMembersList } = require('../controllers/officeRnD'); + const { calculateDoorLockCharges } = require('../services/integration/doorLockCharges'); const express = require('express'); @@ -14,10 +16,13 @@ router.post('/doorLock/upload', uploadDoorLockData); router.get('/integration/mappings', getKnownOfficeResourceMappings); router.post('/integration/mappings', addNewMapping); -router.get('/integration/report/allIncidents', getAllIncidents); +router.get('/integration/report/member/:memberId/:startDate/:endDate', getMemberIncidents); +router.get('/integration/report/allIncidents/:startDate/:endDate', getAllIncidents); router.get('/integration/report/unlockedIncidents', getUnlockedIncidents); router.get('/integration/report/unscheduledIncidents', getUnscheduledIncidents); +router.get('/officeRnD/membersList', fetchMembersList); + // temporary route, manually trigger door lock charge calculations router.get('/calculate', (req, res) => { calculateDoorLockCharges(); res.send();}); diff --git a/services/integration/reports.js b/services/integration/reports.js index ce54691..eee8f68 100644 --- a/services/integration/reports.js +++ b/services/integration/reports.js @@ -3,23 +3,41 @@ const moment = require('moment-timezone'); const db = require('../../models/index'); -const { incidentType } = require('../../constants/constants'); +const Op = require('sequelize').Op; + +const { incidentType, UI_TIMEZONE, DEFAULT_DATE_FORMAT, integrationServiceErrors } = require('../../constants/constants'); const { fetchAllMembers } = require('../officeRnD/members'); const { fetchOffices, fetchResources } = require('../officeRnD/resources'); -const getUnlockedIncidents = () => { +const getUnlockedIncidents = (startDate, endDate, memberId) => { const attributes = ['id', 'memberId', 'resourceId', 'bookingStart', 'bookingEnd', 'incidentLevel', 'incidentLevelPrice']; + const filters = {}; + + if (startDate && endDate) { + filters.bookingStart = { + [Op.and]: { + [Op.gte]: startDate.utc().toISOString(), + [Op.lte]: endDate.utc().toISOString(), + } + } + } + + if (memberId){ + filters.memberId = memberId; + } + return db.unlockedIncident.findAll({ attributes, + where: filters, sort: [ ['bookingStart', 'ASC'] ] }); }; -const getUnscheduledIncidents = () => { +const getUnscheduledIncidents = (startDate, endDate, memberId) => { const attributes = [ 'id', 'memberId', @@ -33,8 +51,24 @@ const getUnscheduledIncidents = () => { 'totalChargeFee' ]; + const filters = {}; + + if (startDate && endDate) { + filters.bookingStart = { + [Op.and]: { + [Op.gte]: startDate.utc().toISOString(), + [Op.lte]: endDate.utc().toISOString(), + } + } + } + + if (memberId){ + filters.memberId = memberId; + } + return db.unscheduledIncident.findAll({ attributes, + where: filters, sort: [ ['bookingStart', 'ASC'] ] @@ -42,13 +76,24 @@ const getUnscheduledIncidents = () => { }; const formatTime = (timestamp) => { - const timezone = process.env.UI_TIMEZONE || 'America/Los_Angeles'; - return moment.tz(timestamp, timezone).format('MM/DD/YYYY hh:mm a'); + return moment.tz(timestamp, UI_TIMEZONE).format('MM/DD/YYYY hh:mm a'); }; -const getAllDoorLockIncidents = () => { +const getAllDoorLockIncidents = (dateRange, memberId) => { return new Promise ((resolve, reject) => { - const dataFetchJobs = [fetchAllMembers(), fetchOffices(), fetchResources(), getUnlockedIncidents(), getUnscheduledIncidents()]; + let startDate, endDate; + + if (dateRange.startDate && dateRange.endDate){ + startDate = moment.tz(dateRange.startDate, DEFAULT_DATE_FORMAT, UI_TIMEZONE); + endDate = moment.tz(dateRange.endDate, DEFAULT_DATE_FORMAT, UI_TIMEZONE); + + if (!startDate.isValid() || !endDate.isValid() || endDate.isBefore(startDate)){ + reject(integrationServiceErrors.INVALID_DATE_RANGE); + return; + } + } + + const dataFetchJobs = [fetchAllMembers(), fetchOffices(), fetchResources(), getUnlockedIncidents(startDate, endDate, memberId), getUnscheduledIncidents(startDate, endDate, memberId)]; Promise.all(dataFetchJobs) .then((data) => {