From 69cc120f54a92fdeb9ecbccb354bf55d6fc0d540 Mon Sep 17 00:00:00 2001 From: Bilal Catic Date: Mon, 17 Jun 2019 20:10:57 +0200 Subject: [PATCH 1/8] add date range picker to the incidents screen --- client/package.json | 1 + .../src/components/DateRangePicker/index.js | 114 ++++++++++++++++++ client/src/constants/constants.js | 1 + client/src/scenes/IncidentsReport/index.js | 14 ++- .../src/store/actions/integrationActions.js | 6 +- client/yarn.lock | 4 + 6 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 client/src/components/DateRangePicker/index.js create mode 100644 client/src/constants/constants.js 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..442a700 --- /dev/null +++ b/client/src/components/DateRangePicker/index.js @@ -0,0 +1,114 @@ +import React, { Component } from 'react'; +import moment from 'moment'; + +import { Form, Message } 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/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/scenes/IncidentsReport/index.js b/client/src/scenes/IncidentsReport/index.js index 09a76cb..d12cd01 100644 --- a/client/src/scenes/IncidentsReport/index.js +++ b/client/src/scenes/IncidentsReport/index.js @@ -1,19 +1,20 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; -import { Container, Loader } from 'semantic-ui-react'; +import {Container, Loader} from 'semantic-ui-react'; import ReactTable from 'react-table'; import 'react-table/react-table.css'; import MainMenu from '../../components/MainMenu'; +import DateRangePicker from '../../components/DateRangePicker'; + 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 () { @@ -56,6 +57,9 @@ class IncidentsReport extends Component { case UNSCHEDULED_INCIDENT: cellValue = `${timeIntervalsToCharge} x 5 min`; break; + default: + cellValue = ''; + break; } break; @@ -82,6 +86,8 @@ class IncidentsReport extends Component {

Incidents Report


+ +
{ !pendingIncidents && incidents && diff --git a/client/src/store/actions/integrationActions.js b/client/src/store/actions/integrationActions.js index 599573f..395186f 100644 --- a/client/src/store/actions/integrationActions.js +++ b/client/src/store/actions/integrationActions.js @@ -36,9 +36,11 @@ export const addNewMapping = (dispatch, mapping) => { }); }; -export const fetchIncidents = (dispatch) => { +export const fetchIncidents = (dispatch, dateRange) => { dispatch({type: FETCH_INCIDENTS_PENDING}); - API.get('integration/report/allIncidents') + API.get('integration/report/allIncidents', { + dateRange + }) .then(response => { dispatch({type: FETCH_INCIDENTS_SUCCESS, payload: response.data}); }) 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" -- 2.47.3 From 1371ab0580036a37b49d1a848e3b9a08e84736c9 Mon Sep 17 00:00:00 2001 From: Bilal Catic Date: Tue, 18 Jun 2019 10:14:11 +0200 Subject: [PATCH 2/8] fix fetching incidents for specific dates from frontend --- client/src/scenes/IncidentsReport/index.js | 2 +- client/src/store/actions/integrationActions.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/scenes/IncidentsReport/index.js b/client/src/scenes/IncidentsReport/index.js index d12cd01..4317159 100644 --- a/client/src/scenes/IncidentsReport/index.js +++ b/client/src/scenes/IncidentsReport/index.js @@ -108,7 +108,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/store/actions/integrationActions.js b/client/src/store/actions/integrationActions.js index 395186f..b79c15e 100644 --- a/client/src/store/actions/integrationActions.js +++ b/client/src/store/actions/integrationActions.js @@ -37,10 +37,10 @@ export const addNewMapping = (dispatch, mapping) => { }; export const fetchIncidents = (dispatch, dateRange) => { + const { startDate, endDate } = dateRange; + dispatch({type: FETCH_INCIDENTS_PENDING}); - API.get('integration/report/allIncidents', { - dateRange - }) + API.get(`integration/report/allIncidents/${startDate}/${endDate}`) .then(response => { dispatch({type: FETCH_INCIDENTS_SUCCESS, payload: response.data}); }) -- 2.47.3 From 16a62b35de5317ff7b3ce4376fddc24ac45036ae Mon Sep 17 00:00:00 2001 From: Bilal Catic Date: Tue, 18 Jun 2019 10:32:17 +0200 Subject: [PATCH 3/8] fix fetching incidents for specific date in backend --- constants/constants.js | 7 +++++ controllers/integration.js | 7 ++++- routes/index.js | 2 +- services/integration/reports.js | 47 ++++++++++++++++++++++++++++----- 4 files changed, 54 insertions(+), 9 deletions(-) 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..7ab7f53 100644 --- a/controllers/integration.js +++ b/controllers/integration.js @@ -33,7 +33,12 @@ 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); }) diff --git a/routes/index.js b/routes/index.js index c1b82eb..fb0fd72 100644 --- a/routes/index.js +++ b/routes/index.js @@ -14,7 +14,7 @@ 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/allIncidents/:startDate/:endDate', getAllIncidents); router.get('/integration/report/unlockedIncidents', getUnlockedIncidents); router.get('/integration/report/unscheduledIncidents', getUnscheduledIncidents); diff --git a/services/integration/reports.js b/services/integration/reports.js index ce54691..1340fdd 100644 --- a/services/integration/reports.js +++ b/services/integration/reports.js @@ -3,23 +3,35 @@ 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) => { const attributes = ['id', 'memberId', 'resourceId', 'bookingStart', 'bookingEnd', 'incidentLevel', 'incidentLevelPrice']; + const filters = (startDate && endDate) ? { + bookingStart: { + [Op.and]: { + [Op.gte]: startDate.utc().toISOString(), + [Op.lte]: endDate.utc().toISOString(), + } + }, + } : null; + return db.unlockedIncident.findAll({ attributes, + where: filters, sort: [ ['bookingStart', 'ASC'] ] }); }; -const getUnscheduledIncidents = () => { +const getUnscheduledIncidents = (startDate, endDate) => { const attributes = [ 'id', 'memberId', @@ -33,8 +45,18 @@ const getUnscheduledIncidents = () => { 'totalChargeFee' ]; + const filters = (startDate && endDate) ? { + bookingStart: { + [Op.and]: { + [Op.gte]: startDate.utc().toISOString(), + [Op.lte]: endDate.utc().toISOString(), + } + }, + } : null; + return db.unscheduledIncident.findAll({ attributes, + where: filters, sort: [ ['bookingStart', 'ASC'] ] @@ -42,13 +64,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) => { 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), getUnscheduledIncidents(startDate, endDate)]; Promise.all(dataFetchJobs) .then((data) => { -- 2.47.3 From e5e86163ead47f1ba6af30e6fba9585b0abadfb3 Mon Sep 17 00:00:00 2001 From: Bilal Catic Date: Tue, 18 Jun 2019 15:25:24 +0200 Subject: [PATCH 4/8] improve date picker layout --- .../src/components/DateRangePicker/index.js | 66 +++++++++++-------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/client/src/components/DateRangePicker/index.js b/client/src/components/DateRangePicker/index.js index 442a700..74d23f7 100644 --- a/client/src/components/DateRangePicker/index.js +++ b/client/src/components/DateRangePicker/index.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import moment from 'moment'; -import { Form, Message } from 'semantic-ui-react'; +import { Form, Message, Grid } from 'semantic-ui-react'; import { defaultDateFormat } from '../../constants/constants'; @@ -81,31 +81,45 @@ class DateRangePicker extends Component { return (
- - - - - { error && - - Wrong date -

{startDateLabel} has to be before {endDateLabel}

-
- } - {buttonLabel} + + + + + + + + + + + + { error && + + + + Wrong date +

{startDateLabel} has to be before {endDateLabel}

+
+
+
+ } + + + {buttonLabel} + + +
); } -- 2.47.3 From 82aad709121c44c58632a0e609922709ef93ac2c Mon Sep 17 00:00:00 2001 From: Bilal Catic Date: Tue, 18 Jun 2019 15:27:18 +0200 Subject: [PATCH 5/8] create and use component for member incidents --- .../components/MemberIncidentsTable/index.js | 93 +++++++++++++++++++ client/src/scenes/IncidentsReport/index.js | 78 +--------------- 2 files changed, 96 insertions(+), 75 deletions(-) create mode 100644 client/src/components/MemberIncidentsTable/index.js diff --git a/client/src/components/MemberIncidentsTable/index.js b/client/src/components/MemberIncidentsTable/index.js new file mode 100644 index 0000000..1586abf --- /dev/null +++ b/client/src/components/MemberIncidentsTable/index.js @@ -0,0 +1,93 @@ +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, incidents, title } = 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; + 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/scenes/IncidentsReport/index.js b/client/src/scenes/IncidentsReport/index.js index 4317159..6501d47 100644 --- a/client/src/scenes/IncidentsReport/index.js +++ b/client/src/scenes/IncidentsReport/index.js @@ -1,15 +1,12 @@ 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 { onDatesUpdate(dateRange) { @@ -20,67 +17,6 @@ class IncidentsReport extends Component { 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; - 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 ( @@ -88,15 +24,7 @@ class IncidentsReport extends Component {

- - { - !pendingIncidents && incidents && - - } +
); } -- 2.47.3 From 785e336e55a1b1881f451bf26e4b63cc737ccee3 Mon Sep 17 00:00:00 2001 From: Bilal Catic Date: Tue, 18 Jun 2019 15:28:04 +0200 Subject: [PATCH 6/8] add structure for member practice summary report --- client/src/constants/menuItems.js | 7 +++ .../components/MemberSelector.js | 20 ++++++++ .../components/MemberSummary.js | 9 ++++ .../src/scenes/PracticeSummaryReport/index.js | 49 +++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 client/src/scenes/PracticeSummaryReport/components/MemberSelector.js create mode 100644 client/src/scenes/PracticeSummaryReport/components/MemberSummary.js create mode 100644 client/src/scenes/PracticeSummaryReport/index.js 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/PracticeSummaryReport/components/MemberSelector.js b/client/src/scenes/PracticeSummaryReport/components/MemberSelector.js new file mode 100644 index 0000000..fe1781a --- /dev/null +++ b/client/src/scenes/PracticeSummaryReport/components/MemberSelector.js @@ -0,0 +1,20 @@ +import React, { Component } from 'react'; +import { Dropdown, Form } from 'semantic-ui-react'; + +class MemberSelector extends Component { + render(){ + return ( +
+ + + + ); + } +} + +export default 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..14c8bcd --- /dev/null +++ b/client/src/scenes/PracticeSummaryReport/index.js @@ -0,0 +1,49 @@ +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'; + +class PracticeSummaryReport extends Component { + render () { + return ( + + +

Practice Summary Report

+
+ + + + + + + + + + + + + + + + + + + + +
+ ); + } +} + +const mapStateToProps = (state) => ({ +}); + +const mapDispatchToProps = (dispatch) => ({ +}); + +export default connect(mapStateToProps, mapDispatchToProps)(PracticeSummaryReport); -- 2.47.3 From 0ccd2ff55c8b95cfee67df284474152db83ecb4f Mon Sep 17 00:00:00 2001 From: Bilal Catic Date: Tue, 18 Jun 2019 23:59:00 +0200 Subject: [PATCH 7/8] handle fetching members list and members incidents list --- controllers/integration.js | 18 +++++++++++++++++ controllers/officeRnD.js | 18 +++++++++++++++++ routes/index.js | 7 ++++++- services/integration/reports.js | 36 ++++++++++++++++++++++----------- 4 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 controllers/officeRnD.js diff --git a/controllers/integration.js b/controllers/integration.js index 7ab7f53..fd2cde0 100644 --- a/controllers/integration.js +++ b/controllers/integration.js @@ -48,6 +48,23 @@ const getAllIncidents = (req, res) => { }); }; +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); + }) + .catch((error) => { + console.log(error); + res.send([]); + }); +}; + const getUnlockedIncidents = (req, res) => { }; @@ -62,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 fb0fd72..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/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 1340fdd..eee8f68 100644 --- a/services/integration/reports.js +++ b/services/integration/reports.js @@ -10,17 +10,23 @@ const { incidentType, UI_TIMEZONE, DEFAULT_DATE_FORMAT, integrationServiceErrors const { fetchAllMembers } = require('../officeRnD/members'); const { fetchOffices, fetchResources } = require('../officeRnD/resources'); -const getUnlockedIncidents = (startDate, endDate) => { +const getUnlockedIncidents = (startDate, endDate, memberId) => { const attributes = ['id', 'memberId', 'resourceId', 'bookingStart', 'bookingEnd', 'incidentLevel', 'incidentLevelPrice']; - const filters = (startDate && endDate) ? { - bookingStart: { + const filters = {}; + + if (startDate && endDate) { + filters.bookingStart = { [Op.and]: { [Op.gte]: startDate.utc().toISOString(), [Op.lte]: endDate.utc().toISOString(), } - }, - } : null; + } + } + + if (memberId){ + filters.memberId = memberId; + } return db.unlockedIncident.findAll({ attributes, @@ -31,7 +37,7 @@ const getUnlockedIncidents = (startDate, endDate) => { }); }; -const getUnscheduledIncidents = (startDate, endDate) => { +const getUnscheduledIncidents = (startDate, endDate, memberId) => { const attributes = [ 'id', 'memberId', @@ -45,14 +51,20 @@ const getUnscheduledIncidents = (startDate, endDate) => { 'totalChargeFee' ]; - const filters = (startDate && endDate) ? { - bookingStart: { + const filters = {}; + + if (startDate && endDate) { + filters.bookingStart = { [Op.and]: { [Op.gte]: startDate.utc().toISOString(), [Op.lte]: endDate.utc().toISOString(), } - }, - } : null; + } + } + + if (memberId){ + filters.memberId = memberId; + } return db.unscheduledIncident.findAll({ attributes, @@ -67,7 +79,7 @@ const formatTime = (timestamp) => { return moment.tz(timestamp, UI_TIMEZONE).format('MM/DD/YYYY hh:mm a'); }; -const getAllDoorLockIncidents = (dateRange) => { +const getAllDoorLockIncidents = (dateRange, memberId) => { return new Promise ((resolve, reject) => { let startDate, endDate; @@ -81,7 +93,7 @@ const getAllDoorLockIncidents = (dateRange) => { } } - const dataFetchJobs = [fetchAllMembers(), fetchOffices(), fetchResources(), getUnlockedIncidents(startDate, endDate), getUnscheduledIncidents(startDate, endDate)]; + const dataFetchJobs = [fetchAllMembers(), fetchOffices(), fetchResources(), getUnlockedIncidents(startDate, endDate, memberId), getUnscheduledIncidents(startDate, endDate, memberId)]; Promise.all(dataFetchJobs) .then((data) => { -- 2.47.3 From 4166ff7a4833cc6bd99767fa33289adaea06f68b Mon Sep 17 00:00:00 2001 From: Bilal Catic Date: Wed, 19 Jun 2019 00:23:40 +0200 Subject: [PATCH 8/8] show incidents for selected member and selected dates --- .../components/MemberIncidentsTable/index.js | 3 +- .../components/MemberSelector.js | 40 ++++++++++++++++- .../src/scenes/PracticeSummaryReport/index.js | 44 +++++++++++++++++-- .../src/store/actions/integrationActions.js | 30 +++++++++++++ client/src/store/constants.js | 8 ++++ client/src/store/reducers/index.js | 4 ++ .../store/reducers/memberIncidentsReducer.js | 38 ++++++++++++++++ .../src/store/reducers/membersListReducer.js | 38 ++++++++++++++++ 8 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 client/src/store/reducers/memberIncidentsReducer.js create mode 100644 client/src/store/reducers/membersListReducer.js diff --git a/client/src/components/MemberIncidentsTable/index.js b/client/src/components/MemberIncidentsTable/index.js index 1586abf..2e6b44f 100644 --- a/client/src/components/MemberIncidentsTable/index.js +++ b/client/src/components/MemberIncidentsTable/index.js @@ -13,7 +13,8 @@ import { const MemberIncidentsTable = props => { - const { loading, incidents, title } = props; + const { loading, title } = props; + const incidents = props.incidents ? props.incidents : []; const columns = []; if (incidents && incidents.length > 0){ diff --git a/client/src/scenes/PracticeSummaryReport/components/MemberSelector.js b/client/src/scenes/PracticeSummaryReport/components/MemberSelector.js index fe1781a..2c439b3 100644 --- a/client/src/scenes/PracticeSummaryReport/components/MemberSelector.js +++ b/client/src/scenes/PracticeSummaryReport/components/MemberSelector.js @@ -1,20 +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 (
); } } -export default MemberSelector; +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/index.js b/client/src/scenes/PracticeSummaryReport/index.js index 14c8bcd..baa1e42 100644 --- a/client/src/scenes/PracticeSummaryReport/index.js +++ b/client/src/scenes/PracticeSummaryReport/index.js @@ -8,8 +8,39 @@ 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 ( @@ -18,10 +49,10 @@ class PracticeSummaryReport extends Component { - + - + @@ -31,7 +62,11 @@ class PracticeSummaryReport extends Component { - + @@ -41,9 +76,12 @@ class PracticeSummaryReport extends Component { } 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 b79c15e..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'; @@ -48,3 +54,27 @@ export const fetchIncidents = (dispatch, dateRange) => { 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; + } +}; -- 2.47.3