Make Practice Summary Report stub
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.18.0",
|
"axios": "^0.18.0",
|
||||||
"fuse.js": "^3.4.5",
|
"fuse.js": "^3.4.5",
|
||||||
|
"moment": "^2.24.0",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-dom": "^16.8.6",
|
"react-dom": "^16.8.6",
|
||||||
"react-redux": "^7.0.3",
|
"react-redux": "^7.0.3",
|
||||||
|
|||||||
128
client/src/components/DateRangePicker/index.js
Normal file
128
client/src/components/DateRangePicker/index.js
Normal file
@@ -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 (
|
||||||
|
<Form>
|
||||||
|
<Grid columns="equal">
|
||||||
|
<Grid.Row>
|
||||||
|
<Grid.Column>
|
||||||
|
<label>{startDateLabel}</label>
|
||||||
|
<Form.Input
|
||||||
|
fluid
|
||||||
|
required
|
||||||
|
type="date"
|
||||||
|
value={startDateValue}
|
||||||
|
onChange={this.onStartDateChange.bind(this)}
|
||||||
|
/>
|
||||||
|
</Grid.Column>
|
||||||
|
<Grid.Column>
|
||||||
|
<label>{endDateLabel}</label>
|
||||||
|
<Form.Input
|
||||||
|
fluid
|
||||||
|
required
|
||||||
|
type="date"
|
||||||
|
value={endDateValue}
|
||||||
|
onChange={this.onEndDateChange.bind(this)}
|
||||||
|
/>
|
||||||
|
</Grid.Column>
|
||||||
|
</Grid.Row>
|
||||||
|
{ error &&
|
||||||
|
<Grid.Row>
|
||||||
|
<Grid.Column>
|
||||||
|
<Message color="orange" onDismiss={this.onDismiss.bind(this)}>
|
||||||
|
<Message.Header>Wrong date</Message.Header>
|
||||||
|
<p><b>{startDateLabel}</b> has to be before <b>{endDateLabel}</b></p>
|
||||||
|
</Message>
|
||||||
|
</Grid.Column>
|
||||||
|
</Grid.Row>
|
||||||
|
}
|
||||||
|
<Grid.Row>
|
||||||
|
<Grid.Column>
|
||||||
|
<Form.Button onClick={this.onButtonClick.bind(this)}>{buttonLabel}</Form.Button>
|
||||||
|
</Grid.Column>
|
||||||
|
</Grid.Row>
|
||||||
|
</Grid>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DateRangePicker;
|
||||||
94
client/src/components/MemberIncidentsTable/index.js
Normal file
94
client/src/components/MemberIncidentsTable/index.js
Normal file
@@ -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 <div style={{ textAlign: columnContentsAlignment }}>{cellValue}</div>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h4>{title}</h4>
|
||||||
|
<Loader active={loading} />
|
||||||
|
{
|
||||||
|
!loading && incidents &&
|
||||||
|
<ReactTable
|
||||||
|
data={incidents}
|
||||||
|
multiSort={false}
|
||||||
|
columns={columns}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemberIncidentsTable;
|
||||||
1
client/src/constants/constants.js
Normal file
1
client/src/constants/constants.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const defaultDateFormat = 'YYYY-MM-DD';
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import UploadDLockData from '../scenes/UploadDLockData';
|
import UploadDLockData from '../scenes/UploadDLockData';
|
||||||
import Home from '../scenes/Home';
|
import Home from '../scenes/Home';
|
||||||
import IncidentsReport from '../scenes/IncidentsReport';
|
import IncidentsReport from '../scenes/IncidentsReport';
|
||||||
|
import PracticeSummaryReport from '../scenes/PracticeSummaryReport';
|
||||||
|
|
||||||
export const mainMenuItems = [
|
export const mainMenuItems = [
|
||||||
{
|
{
|
||||||
@@ -9,6 +10,12 @@ export const mainMenuItems = [
|
|||||||
url: '/',
|
url: '/',
|
||||||
component: Home,
|
component: Home,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'practiceSummaryReport',
|
||||||
|
title: 'Practice Summary Report',
|
||||||
|
url: '/practice-summary-report',
|
||||||
|
component: PracticeSummaryReport,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'report',
|
id: 'report',
|
||||||
title: 'Incidents Report',
|
title: 'Incidents Report',
|
||||||
|
|||||||
@@ -1,96 +1,30 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Container, Loader } from 'semantic-ui-react';
|
import { Container } from 'semantic-ui-react';
|
||||||
import ReactTable from 'react-table';
|
|
||||||
import 'react-table/react-table.css';
|
|
||||||
|
|
||||||
import MainMenu from '../../components/MainMenu';
|
import MainMenu from '../../components/MainMenu';
|
||||||
|
import DateRangePicker from '../../components/DateRangePicker';
|
||||||
|
import MemberIncidentsTable from '../../components/MemberIncidentsTable';
|
||||||
|
|
||||||
import { fetchIncidents } from '../../store/actions';
|
import { fetchIncidents } from '../../store/actions';
|
||||||
import { incidentsReportHeaderTitles } from '../../constants/menuItems';
|
|
||||||
import { incidentDescriptions, incidentLevelDescriptions, UNSCHEDULED_INCIDENT, UNLOCKED_INCIDENT } from '../../constants/enums';
|
|
||||||
|
|
||||||
class IncidentsReport extends Component {
|
class IncidentsReport extends Component {
|
||||||
|
onDatesUpdate(dateRange) {
|
||||||
componentDidMount() {
|
|
||||||
const { fetchIncidents } = this.props;
|
const { fetchIncidents } = this.props;
|
||||||
fetchIncidents();
|
fetchIncidents(dateRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { pendingIncidents, incidents } = this.props;
|
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 <div style={{ textAlign: columnContentsAlignment }}>{cellValue}</div>
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<MainMenu/>
|
<MainMenu/>
|
||||||
<h3>Incidents Report</h3>
|
<h3>Incidents Report</h3>
|
||||||
<hr/>
|
<hr/>
|
||||||
<Loader active={pendingIncidents} />
|
<DateRangePicker buttonLabel="Show report" onDatesUpdate={this.onDatesUpdate.bind(this)} />
|
||||||
{
|
<br/>
|
||||||
!pendingIncidents && incidents &&
|
<MemberIncidentsTable loading={pendingIncidents} incidents={incidents} />
|
||||||
<ReactTable
|
|
||||||
data={incidents}
|
|
||||||
multiSort={false}
|
|
||||||
columns={columns}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -102,7 +36,7 @@ const mapStateToProps = (state) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
fetchIncidents: () => fetchIncidents(dispatch),
|
fetchIncidents: (dateRange) => fetchIncidents(dispatch, dateRange),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(IncidentsReport);
|
export default connect(mapStateToProps, mapDispatchToProps)(IncidentsReport);
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Form>
|
||||||
|
<label>{'\u00A0'}</label>
|
||||||
|
<Dropdown
|
||||||
|
options={dropdownOptions}
|
||||||
|
placeholder="Select Member"
|
||||||
|
selection
|
||||||
|
search
|
||||||
|
fluid
|
||||||
|
onChange={this.onMemberSelectionChange.bind(this)}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => ({
|
||||||
|
members: state.membersList.result,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
fetchMembersList: () => fetchMembersList(dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(MemberSelector);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
class MemberSummary extends Component {
|
||||||
|
render() {
|
||||||
|
return (<h4>Member Summary</h4>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MemberSummary;
|
||||||
87
client/src/scenes/PracticeSummaryReport/index.js
Normal file
87
client/src/scenes/PracticeSummaryReport/index.js
Normal file
@@ -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 (
|
||||||
|
<Container>
|
||||||
|
<MainMenu/>
|
||||||
|
<h3>Practice Summary Report</h3>
|
||||||
|
<hr/>
|
||||||
|
<Grid stackable columns="equal">
|
||||||
|
<Grid.Row>
|
||||||
|
<Grid.Column>
|
||||||
|
<MemberSelector onMemberSelect={this.onMemberSelectionUpdate.bind(this)} />
|
||||||
|
</Grid.Column>
|
||||||
|
<Grid.Column>
|
||||||
|
<DateRangePicker onDatesUpdate={this.onDateRangeUpdate.bind(this)}/>
|
||||||
|
</Grid.Column>
|
||||||
|
</Grid.Row>
|
||||||
|
<Grid.Row>
|
||||||
|
<Grid.Column>
|
||||||
|
<MemberSummary />
|
||||||
|
</Grid.Column>
|
||||||
|
</Grid.Row>
|
||||||
|
<Grid.Row>
|
||||||
|
<Grid.Column>
|
||||||
|
<MemberIncidentsTable
|
||||||
|
title="Detail list"
|
||||||
|
incidents={memberIncidents}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</Grid.Column>
|
||||||
|
</Grid.Row>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -8,6 +8,12 @@ import {
|
|||||||
FETCH_INCIDENTS_PENDING,
|
FETCH_INCIDENTS_PENDING,
|
||||||
FETCH_INCIDENTS_SUCCESS,
|
FETCH_INCIDENTS_SUCCESS,
|
||||||
FETCH_INCIDENTS_FAILED,
|
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';
|
} from '../constants';
|
||||||
|
|
||||||
import API from '../../utilities/api';
|
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});
|
dispatch({type: FETCH_INCIDENTS_PENDING});
|
||||||
API.get('integration/report/allIncidents')
|
API.get(`integration/report/allIncidents/${startDate}/${endDate}`)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
dispatch({type: FETCH_INCIDENTS_SUCCESS, payload: response.data});
|
dispatch({type: FETCH_INCIDENTS_SUCCESS, payload: response.data});
|
||||||
})
|
})
|
||||||
@@ -46,3 +54,27 @@ export const fetchIncidents = (dispatch) => {
|
|||||||
dispatch({type: FETCH_INCIDENTS_FAILED, payload: error.response});
|
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});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -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_PENDING = 'FETCH_INCIDENTS_PENDING';
|
||||||
export const FETCH_INCIDENTS_SUCCESS = 'FETCH_INCIDENTS_SUCCESS';
|
export const FETCH_INCIDENTS_SUCCESS = 'FETCH_INCIDENTS_SUCCESS';
|
||||||
export const FETCH_INCIDENTS_FAILED = 'FETCH_INCIDENTS_FAILED';
|
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';
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ import { doorLockData} from './doorLockReducers';
|
|||||||
import { mappingsData } from './mappingsReducer';
|
import { mappingsData } from './mappingsReducer';
|
||||||
import { addMapping } from './addMappingReducer';
|
import { addMapping } from './addMappingReducer';
|
||||||
import { incidentsReport } from './incidentsReportReducer';
|
import { incidentsReport } from './incidentsReportReducer';
|
||||||
|
import { membersList } from './membersListReducer';
|
||||||
|
import { memberIncidents} from './memberIncidentsReducer';
|
||||||
|
|
||||||
export const rootReducer = combineReducers({
|
export const rootReducer = combineReducers({
|
||||||
doorLockData,
|
doorLockData,
|
||||||
mappingsData,
|
mappingsData,
|
||||||
addMapping,
|
addMapping,
|
||||||
incidentsReport,
|
incidentsReport,
|
||||||
|
membersList,
|
||||||
|
memberIncidents,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
38
client/src/store/reducers/memberIncidentsReducer.js
Normal file
38
client/src/store/reducers/memberIncidentsReducer.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
38
client/src/store/reducers/membersListReducer.js
Normal file
38
client/src/store/reducers/membersListReducer.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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:
|
dependencies:
|
||||||
minimist "0.0.8"
|
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:
|
move-concurrently@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
|
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ const integrationServiceErrors = {
|
|||||||
FAILED_TO_SAVE_BOOKINGS: 'Failed to save booking reservations',
|
FAILED_TO_SAVE_BOOKINGS: 'Failed to save booking reservations',
|
||||||
FAILED_TO_SAVE_DOOR_LOCK_ENTRIES: 'Failed to save door lock entries',
|
FAILED_TO_SAVE_DOOR_LOCK_ENTRIES: 'Failed to save door lock entries',
|
||||||
FAILED_TO_SAVE_DATA_GENERIC: 'Failed to save data',
|
FAILED_TO_SAVE_DATA_GENERIC: 'Failed to save data',
|
||||||
|
INVALID_DATE_RANGE: 'Dates in date range are invalid',
|
||||||
};
|
};
|
||||||
|
|
||||||
const incidentType = {
|
const incidentType = {
|
||||||
@@ -64,6 +65,10 @@ const incidentType = {
|
|||||||
UNSCHEDULED_INCIDENT: 3,
|
UNSCHEDULED_INCIDENT: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const UI_TIMEZONE = process.env.UI_TIMEZONE || 'America/Los_Angeles';
|
||||||
|
|
||||||
|
const DEFAULT_DATE_FORMAT = 'YYYY-MM-DD';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
VALID_CSV_HEADERS,
|
VALID_CSV_HEADERS,
|
||||||
USER_ENTRY_EVENT,
|
USER_ENTRY_EVENT,
|
||||||
@@ -75,4 +80,6 @@ module.exports = {
|
|||||||
unlockedIncidentLevelsPrices,
|
unlockedIncidentLevelsPrices,
|
||||||
integrationServiceErrors,
|
integrationServiceErrors,
|
||||||
incidentType,
|
incidentType,
|
||||||
|
UI_TIMEZONE,
|
||||||
|
DEFAULT_DATE_FORMAT,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,7 +33,29 @@ const addNewMapping = (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getAllIncidents = (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) => {
|
.then((incidents) => {
|
||||||
res.send(incidents);
|
res.send(incidents);
|
||||||
})
|
})
|
||||||
@@ -57,4 +79,5 @@ module.exports = {
|
|||||||
getAllIncidents,
|
getAllIncidents,
|
||||||
getUnscheduledIncidents,
|
getUnscheduledIncidents,
|
||||||
getUnlockedIncidents,
|
getUnlockedIncidents,
|
||||||
|
getMemberIncidents,
|
||||||
};
|
};
|
||||||
|
|||||||
18
controllers/officeRnD.js
Normal file
18
controllers/officeRnD.js
Normal file
@@ -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,
|
||||||
|
};
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
const { apiStatusCheck } = require('../controllers/apiStatusCheck');
|
const { apiStatusCheck } = require('../controllers/apiStatusCheck');
|
||||||
const { uploadDoorLockData } = require('../controllers/doorLock');
|
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 { calculateDoorLockCharges } = require('../services/integration/doorLockCharges');
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
@@ -14,10 +16,13 @@ router.post('/doorLock/upload', uploadDoorLockData);
|
|||||||
router.get('/integration/mappings', getKnownOfficeResourceMappings);
|
router.get('/integration/mappings', getKnownOfficeResourceMappings);
|
||||||
router.post('/integration/mappings', addNewMapping);
|
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/unlockedIncidents', getUnlockedIncidents);
|
||||||
router.get('/integration/report/unscheduledIncidents', getUnscheduledIncidents);
|
router.get('/integration/report/unscheduledIncidents', getUnscheduledIncidents);
|
||||||
|
|
||||||
|
router.get('/officeRnD/membersList', fetchMembersList);
|
||||||
|
|
||||||
// temporary route, manually trigger door lock charge calculations
|
// temporary route, manually trigger door lock charge calculations
|
||||||
router.get('/calculate', (req, res) => { calculateDoorLockCharges(); res.send();});
|
router.get('/calculate', (req, res) => { calculateDoorLockCharges(); res.send();});
|
||||||
|
|
||||||
|
|||||||
@@ -3,23 +3,41 @@
|
|||||||
const moment = require('moment-timezone');
|
const moment = require('moment-timezone');
|
||||||
|
|
||||||
const db = require('../../models/index');
|
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 { fetchAllMembers } = require('../officeRnD/members');
|
||||||
const { fetchOffices, fetchResources } = require('../officeRnD/resources');
|
const { fetchOffices, fetchResources } = require('../officeRnD/resources');
|
||||||
|
|
||||||
const getUnlockedIncidents = () => {
|
const getUnlockedIncidents = (startDate, endDate, memberId) => {
|
||||||
const attributes = ['id', 'memberId', 'resourceId', 'bookingStart', 'bookingEnd', 'incidentLevel', 'incidentLevelPrice'];
|
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({
|
return db.unlockedIncident.findAll({
|
||||||
attributes,
|
attributes,
|
||||||
|
where: filters,
|
||||||
sort: [
|
sort: [
|
||||||
['bookingStart', 'ASC']
|
['bookingStart', 'ASC']
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUnscheduledIncidents = () => {
|
const getUnscheduledIncidents = (startDate, endDate, memberId) => {
|
||||||
const attributes = [
|
const attributes = [
|
||||||
'id',
|
'id',
|
||||||
'memberId',
|
'memberId',
|
||||||
@@ -33,8 +51,24 @@ const getUnscheduledIncidents = () => {
|
|||||||
'totalChargeFee'
|
'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({
|
return db.unscheduledIncident.findAll({
|
||||||
attributes,
|
attributes,
|
||||||
|
where: filters,
|
||||||
sort: [
|
sort: [
|
||||||
['bookingStart', 'ASC']
|
['bookingStart', 'ASC']
|
||||||
]
|
]
|
||||||
@@ -42,13 +76,24 @@ const getUnscheduledIncidents = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const formatTime = (timestamp) => {
|
const formatTime = (timestamp) => {
|
||||||
const timezone = process.env.UI_TIMEZONE || 'America/Los_Angeles';
|
return moment.tz(timestamp, UI_TIMEZONE).format('MM/DD/YYYY hh:mm a');
|
||||||
return moment.tz(timestamp, timezone).format('MM/DD/YYYY hh:mm a');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAllDoorLockIncidents = () => {
|
const getAllDoorLockIncidents = (dateRange, memberId) => {
|
||||||
return new Promise ((resolve, reject) => {
|
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)
|
Promise.all(dataFetchJobs)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user