Make Practice Summary Report stub

This commit is contained in:
Senad Uka
2019-06-19 11:23:58 +02:00
parent 9a8d95dd19
commit 3ec13983c6
20 changed files with 629 additions and 88 deletions

View File

@@ -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",

View 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;

View 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;

View File

@@ -0,0 +1 @@
export const defaultDateFormat = 'YYYY-MM-DD';

View File

@@ -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',

View File

@@ -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 <div style={{ textAlign: columnContentsAlignment }}>{cellValue}</div>
}
});
}
});
}
return (
<Container>
<MainMenu/>
<h3>Incidents Report</h3>
<hr/>
<Loader active={pendingIncidents} />
{
!pendingIncidents && incidents &&
<ReactTable
data={incidents}
multiSort={false}
columns={columns}
/>
}
<DateRangePicker buttonLabel="Show report" onDatesUpdate={this.onDatesUpdate.bind(this)} />
<br/>
<MemberIncidentsTable loading={pendingIncidents} incidents={incidents} />
</Container>
);
}
@@ -102,7 +36,7 @@ const mapStateToProps = (state) => ({
});
const mapDispatchToProps = (dispatch) => ({
fetchIncidents: () => fetchIncidents(dispatch),
fetchIncidents: (dateRange) => fetchIncidents(dispatch, dateRange),
});
export default connect(mapStateToProps, mapDispatchToProps)(IncidentsReport);

View File

@@ -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);

View File

@@ -0,0 +1,9 @@
import React, { Component } from 'react';
class MemberSummary extends Component {
render() {
return (<h4>Member Summary</h4>);
}
}
export default MemberSummary;

View 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);

View File

@@ -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});
});
};

View File

@@ -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';

View File

@@ -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,
});

View 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;
}
};

View 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;
}
};

View File

@@ -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"