Merge branch 'add-door-lock-charges-screen' into 'master'

Show incidents report

See merge request saburly/psihologija!11
This commit was merged in pull request #11.
This commit is contained in:
Bilal Catic
2019-06-17 11:16:40 +00:00
24 changed files with 374 additions and 25 deletions

View File

@@ -10,6 +10,7 @@
"react-redux": "^7.0.3",
"react-router-dom": "^5.0.0",
"react-scripts": "3.0.1",
"react-table": "^6.10.0",
"redux": "^4.0.1",
"redux-thunk": "^2.3.0",
"semantic-ui-css": "^2.4.1",

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { NavLink } from 'react-router-dom';
import { Menu } from 'semantic-ui-react';
import { mainMenuItems } from "../../constants/menuItems";
import { mainMenuItems } from '../../constants/menuItems';
const MainMenu = () =>
(<Menu>

View File

@@ -0,0 +1,17 @@
export const UNLOCKED_INCIDENT = 2;
export const UNSCHEDULED_INCIDENT = 3;
export const incidentDescriptions = {};
incidentDescriptions[UNLOCKED_INCIDENT] = 'User left door unlocked';
incidentDescriptions[UNSCHEDULED_INCIDENT] = 'Unscheduled use';
export const incidentLevelDescriptions = {
UNLOCKED_0: 'First month',
UNLOCKED_1: 'Second month',
UNLOCKED_2: 'Third month',
UNLOCKED_3: 'Fourth month',
UNLOCKED_4: 'Fifth month',
UNLOCKED_5: 'Sixth month',
};

View File

@@ -1,5 +1,6 @@
import UploadDLockData from "../scenes/UploadDLockData";
import Home from "../scenes/Home";
import UploadDLockData from '../scenes/UploadDLockData';
import Home from '../scenes/Home';
import IncidentsReport from '../scenes/IncidentsReport';
export const mainMenuItems = [
{
@@ -8,6 +9,12 @@ export const mainMenuItems = [
url: '/',
component: Home,
},
{
id: 'report',
title: 'Incidents Report',
url: '/incidents-report',
component: IncidentsReport,
},
{
id: 'uploadDLockData',
title: 'DLock',
@@ -15,3 +22,14 @@ export const mainMenuItems = [
component: UploadDLockData,
},
];
export const incidentsReportHeaderTitles = {
officeName: 'Office',
resourceName: 'Room',
bookingStart: 'Reservation Start',
bookingEnd: 'Reservation End',
memberName: 'Member Name',
incidentType: 'Incident Type',
feeDescription: 'Fee description',
totalChargeFee: 'Total Fee',
};

View File

@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { Container, Form } from "semantic-ui-react";
import { Container, Form } from 'semantic-ui-react';
import MainMenu from '../../components/MainMenu';

View File

@@ -0,0 +1,108 @@
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 MainMenu from '../../components/MainMenu';
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() {
const { fetchIncidents } = this.props;
fetchIncidents();
}
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}
/>
}
</Container>
);
}
}
const mapStateToProps = (state) => ({
pendingIncidents: state.incidentsReport.pending,
incidents: state.incidentsReport.result,
});
const mapDispatchToProps = (dispatch) => ({
fetchIncidents: () => fetchIncidents(dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(IncidentsReport);

View File

@@ -1,5 +1,5 @@
import React from 'react';
import MainMenu from "../../components/MainMenu";
import MainMenu from '../../components/MainMenu';
export default function NotFound () {
return (

View File

@@ -1,10 +1,10 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Form } from "semantic-ui-react";
import { Form } from 'semantic-ui-react';
import UnknownMapping from './UnknownMapping';
import { uploadDoorLockData, fetchMappings } from "../../../store/actions";
import { uploadDoorLockData, fetchMappings } from '../../../store/actions';
class FileUpload extends Component {
constructor(props) {

View File

@@ -1,8 +1,8 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {Button, Dropdown, Message} from "semantic-ui-react";
import {Button, Dropdown, Message} from 'semantic-ui-react';
import Fuse from 'fuse.js';
import { addNewMapping } from "../../../store/actions";
import { addNewMapping } from '../../../store/actions';
class UnknownMapping extends Component {
constructor(props) {

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Container, Form } from "semantic-ui-react";
import { Container, Form } from 'semantic-ui-react';
import MainMenu from '../../components/MainMenu';
import FileUpload from './components/FileUpload';

View File

@@ -2,7 +2,7 @@ import {
UPLOAD_DOOR_LOCK_DATA_PENDING,
UPLOAD_DOOR_LOCK_DATA_SUCCESS,
UPLOAD_DOOR_LOCK_DATA_FAILED
} from "../constants";
} from '../constants';
import API from '../../utilities/api';

View File

@@ -5,7 +5,10 @@ import {
ADD_NEW_MAPPING_PENDING,
ADD_NEW_MAPPING_SUCCESS,
ADD_NEW_MAPPING_FAILED,
} from "../constants";
FETCH_INCIDENTS_PENDING,
FETCH_INCIDENTS_SUCCESS,
FETCH_INCIDENTS_FAILED,
} from '../constants';
import API from '../../utilities/api';
@@ -32,3 +35,14 @@ export const addNewMapping = (dispatch, mapping) => {
dispatch({type: ADD_NEW_MAPPING_FAILED, payload: error.response});
});
};
export const fetchIncidents = (dispatch) => {
dispatch({type: FETCH_INCIDENTS_PENDING});
API.get('integration/report/allIncidents')
.then(response => {
dispatch({type: FETCH_INCIDENTS_SUCCESS, payload: response.data});
})
.catch(error => {
dispatch({type: FETCH_INCIDENTS_FAILED, payload: error.response});
});
};

View File

@@ -9,3 +9,7 @@ export const FETCH_MAPPINGS_FAILED = 'FETCH_MAPPINGS_FAILED';
export const ADD_NEW_MAPPING_PENDING = 'ADD_NEW_MAPPING_PENDING';
export const ADD_NEW_MAPPING_SUCCESS = 'ADD_NEW_MAPPING_SUCCESS';
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';

View File

@@ -2,7 +2,7 @@ import {
ADD_NEW_MAPPING_PENDING,
ADD_NEW_MAPPING_SUCCESS,
ADD_NEW_MAPPING_FAILED,
} from "../constants";
} from '../constants';
const initialState = {
pending: false,

View File

@@ -2,7 +2,7 @@ import {
UPLOAD_DOOR_LOCK_DATA_PENDING,
UPLOAD_DOOR_LOCK_DATA_SUCCESS,
UPLOAD_DOOR_LOCK_DATA_FAILED
} from "../constants";
} from '../constants';
const initialState = {
pending: false,

View File

@@ -0,0 +1,38 @@
import {
FETCH_INCIDENTS_PENDING,
FETCH_INCIDENTS_SUCCESS,
FETCH_INCIDENTS_FAILED,
} from '../constants';
const initialState = {
pending: false,
result: null,
error: null,
};
export const incidentsReport = (state, action) => {
state = state || initialState;
action = action || {};
switch(action.type){
case FETCH_INCIDENTS_PENDING:
return Object.assign({}, state, {
pending: true,
error: null,
});
case FETCH_INCIDENTS_SUCCESS:
return Object.assign({}, state, {
pending: false,
result: action.payload,
error: null,
});
case FETCH_INCIDENTS_FAILED:
return Object.assign({}, state, {
pending: false,
result: {},
error: action.payload,
});
default:
return state;
}
};

View File

@@ -1,12 +1,14 @@
import { combineReducers } from "redux";
import { combineReducers } from 'redux';
import { doorLockData} from "./doorLockReducers";
import { mappingsData } from "./mappingsReducer";
import { doorLockData} from './doorLockReducers';
import { mappingsData } from './mappingsReducer';
import { addMapping } from './addMappingReducer';
import { incidentsReport } from './incidentsReportReducer';
export const rootReducer = combineReducers({
doorLockData,
mappingsData,
addMapping,
incidentsReport,
});

View File

@@ -2,7 +2,7 @@ import {
FETCH_MAPPINGS_PENDING,
FETCH_MAPPINGS_SUCCESS,
FETCH_MAPPINGS_FAILED,
} from "../constants";
} from '../constants';
const initialState = {
pending: false,

View File

@@ -2140,7 +2140,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
classnames@^2.2.6:
classnames@^2.2.5, classnames@^2.2.6:
version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
@@ -7095,6 +7095,12 @@ react-scripts@3.0.1:
optionalDependencies:
fsevents "2.0.6"
react-table@^6.10.0:
version "6.10.0"
resolved "https://registry.yarnpkg.com/react-table/-/react-table-6.10.0.tgz#20444b19d8ca3c1a08e7544e5c3a93e4ba56690e"
dependencies:
classnames "^2.2.5"
react@^16.8.6:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react/-/react-16.8.6.tgz#ad6c3a9614fd3a4e9ef51117f54d888da01f2bbe"

View File

@@ -1,6 +1,7 @@
'use strict';
const { getMappingsFromDatabase, fetchOffices, fetchResources, saveNewMappingToDatabase } = require('../services/officeRnD/resources');
const { getAllDoorLockIncidents } = require('../services/integration/reports');
const getKnownOfficeResourceMappings = (req, res) => {
const dataToFetch = [getMappingsFromDatabase(), fetchOffices(), fetchResources() ];
@@ -31,7 +32,29 @@ const addNewMapping = (req, res) => {
}
};
const getAllIncidents = (req, res) => {
getAllDoorLockIncidents()
.then((incidents) => {
res.send(incidents);
})
.catch((error) => {
console.log(error);
res.send([]);
});
};
const getUnlockedIncidents = (req, res) => {
};
const getUnscheduledIncidents = (req, res) => {
};
module.exports = {
getKnownOfficeResourceMappings,
addNewMapping,
getAllIncidents,
getUnscheduledIncidents,
getUnlockedIncidents,
};

View File

@@ -3,7 +3,9 @@ BASIC_AUTH_PASSWORD=password
OFFICE_RnD_TOKEN=token for Office RnD API requests
MAX_BACK_TO_BACK_DIFFERENCE=Time in minutes
EARLIEST_UNLOCK=2
EARLIEST_UNLOCK=Time in minutes
UI_TIMEZONE=Timezone for user interface | https://en.wikipedia.org/wiki/List_of_tz_database_time_zones | example : America/Los_Angeles
UNSCHEDULED_USE_TIME_RESOLUTION=Time in minutes
UNSCHEDULED_USE_CHARGE_FEE=Charge fee

View File

@@ -2,7 +2,7 @@
const { apiStatusCheck } = require('../controllers/apiStatusCheck');
const { uploadDoorLockData } = require('../controllers/doorLock');
const { getKnownOfficeResourceMappings, addNewMapping } = require('../controllers/integration');
const { getKnownOfficeResourceMappings, addNewMapping, getAllIncidents,getUnscheduledIncidents, getUnlockedIncidents } = require('../controllers/integration');
const { calculateDoorLockCharges } = require('../services/integration/doorLockCharges');
const express = require('express');
@@ -14,6 +14,10 @@ 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/unlockedIncidents', getUnlockedIncidents);
router.get('/integration/report/unscheduledIncidents', getUnscheduledIncidents);
// temporary route, manually trigger door lock charge calculations
router.get('/calculate', (req, res) => { calculateDoorLockCharges(); res.send();});

View File

@@ -97,8 +97,8 @@ const createUnlockedIncident = (reservation) => {
const createUnscheduledUseIncident = (reservation, doorLockEntry) => {
return new Promise((resolve, reject) => {
const timeResolution = parseInt(process.env.UNSCHEDULED_USE_TIME_RESOLUTION);
const chargePrice = parseFloat(process.env.UNSCHEDULED_USE_CHARGE_FEE);
const timeResolution = parseInt(process.env.UNSCHEDULED_USE_TIME_RESOLUTION) || 5;
const chargePrice = parseFloat(process.env.UNSCHEDULED_USE_CHARGE_FEE) || 5;
const reservationEndTime = moment(reservation.end);
const lockedTime = moment(doorLockEntry.timestamp);
@@ -352,8 +352,8 @@ const getIncidentData = (reservation) => {
getRelatedDoorLockEntries(reservation.start, doorLockEntriesEndTime, reservation.memberId, reservation.resourceId)
.then((lockEntry) => {
if (lockEntry){
const timeResolution = parseInt(process.env.UNSCHEDULED_USE_TIME_RESOLUTION);
const chargePrice = parseFloat(process.env.UNSCHEDULED_USE_CHARGE_FEE);
const timeResolution = parseInt(process.env.UNSCHEDULED_USE_TIME_RESOLUTION) || 5
const chargePrice = parseFloat(process.env.UNSCHEDULED_USE_CHARGE_FEE) || 5;
const reservationEndTime = moment(reservation.end);
const lockedTime = moment(lockEntry.timestamp);

View File

@@ -0,0 +1,112 @@
'use strict';
const moment = require('moment-timezone');
const db = require('../../models/index');
const { incidentType } = require('../../constants/constants');
const { fetchAllMembers } = require('../officeRnD/members');
const { fetchOffices, fetchResources } = require('../officeRnD/resources');
const getUnlockedIncidents = () => {
const attributes = ['id', 'memberId', 'resourceId', 'bookingStart', 'bookingEnd', 'incidentLevel', 'incidentLevelPrice'];
return db.unlockedIncident.findAll({
attributes,
sort: [
['bookingStart', 'ASC']
]
});
};
const getUnscheduledIncidents = () => {
const attributes = [
'id',
'memberId',
'resourceId',
'bookingStart',
'bookingEnd',
'doorLockEventTimestamp',
'doorLockEventType',
'timeIntervalsToCharge',
'chargePrice',
'totalChargeFee'
];
return db.unscheduledIncident.findAll({
attributes,
sort: [
['bookingStart', 'ASC']
]
});
};
const formatTime = (timestamp) => {
const timezone = process.env.UI_TIMEZONE || 'America/Los_Angeles';
return moment.tz(timestamp, timezone).format('MM/DD/YYYY hh:mm a');
};
const getAllDoorLockIncidents = () => {
return new Promise ((resolve, reject) => {
const dataFetchJobs = [fetchAllMembers(), fetchOffices(), fetchResources(), getUnlockedIncidents(), getUnscheduledIncidents()];
Promise.all(dataFetchJobs)
.then((data) => {
const members = data[0];
const offices = data[1];
const resources = data[2];
const unlockedIncidents = data[3];
const unscheduledIncidents = data[4];
const membersMap = {};
const officesMap = {};
const resourcesMap = {};
members.forEach((member) => membersMap[member.memberId] = member);
offices.forEach((office) => officesMap[office.officeId] = office);
resources.forEach((resource) => resourcesMap[resource.resourceId] = resource);
const allIncidents = [];
unlockedIncidents.forEach((unlockedIncident) => {
allIncidents.push({
incidentId: unlockedIncident.id,
memberId: unlockedIncident.memberId,
memberName: membersMap[unlockedIncident.memberId].name,
resourceName: resourcesMap[unlockedIncident.resourceId].resourceName,
officeName: officesMap[resourcesMap[unlockedIncident.resourceId].officeId].officeName,
bookingStart: formatTime(unlockedIncident.bookingStart),
bookingEnd: formatTime(unlockedIncident.bookingEnd),
incidentType: incidentType.UNLOCKED_INCIDENT,
incidentLevel: unlockedIncident.incidentLevel,
incidentPrice: unlockedIncident.incidentLevelPrice,
});
});
unscheduledIncidents.forEach((unscheduledIncident) => {
allIncidents.push({
incidentId: unscheduledIncident.id,
memberId: unscheduledIncident.memberId,
memberName: membersMap[unscheduledIncident.memberId].name,
resourceName: resourcesMap[unscheduledIncident.resourceId].resourceName,
officeName: officesMap[resourcesMap[unscheduledIncident.resourceId].officeId].officeName,
bookingStart: formatTime(unscheduledIncident.bookingStart),
bookingEnd: formatTime(unscheduledIncident.bookingEnd),
incidentType: incidentType.UNSCHEDULED_INCIDENT,
timeIntervalsToCharge: unscheduledIncident.timeIntervalsToCharge,
chargePrice: unscheduledIncident.chargePrice,
totalChargeFee: unscheduledIncident.totalChargeFee,
});
});
resolve(allIncidents);
})
.catch((error) => reject(error));
});
};
module.exports = {
getUnlockedIncidents,
getUnscheduledIncidents,
getAllDoorLockIncidents,
};