diff --git a/src/components/AddressSearch/AddressSearch.js b/src/components/AddressSearch/AddressSearch.js index 43f1f85707dc5562d63744383ffc2db6264f156a..d61339cbb83c071d3632f46c0505562707a2f94a 100644 --- a/src/components/AddressSearch/AddressSearch.js +++ b/src/components/AddressSearch/AddressSearch.js @@ -93,7 +93,7 @@ class AddressSearch extends Component { )} renderInput={props => ( - + {this.state.isFetching ? : null} diff --git a/src/containers/BGroup/BGroup.js b/src/containers/BGroup/BGroup.js index 8d70fc58b1ec329e56e5beef93e8c3e24d500c41..5a0df2ead7ed2f8fcf8003b4e0def97d13d4eb80 100644 --- a/src/containers/BGroup/BGroup.js +++ b/src/containers/BGroup/BGroup.js @@ -3,11 +3,12 @@ import { connect } from 'react-redux'; import { Link, browserHistory } from 'react-router'; import { bindActionCreators } from 'redux'; import PropTypes from 'prop-types'; -import { Button } from 'reactstrap'; +import { Button, Nav, NavItem, NavLink } from 'reactstrap'; import ReactTable from 'react-table'; import 'react-table/react-table.css'; import { Icon } from 'react-fa'; import ReactTooltip from 'react-tooltip'; +import Loading from '../../components/Loading'; import NavBar from '../../components/NavBar'; import { loadBGroupBuildings, @@ -16,6 +17,7 @@ import { deleteBuildingFromBGroup, deleteBGroup, } from './actions'; +import BGroupProjectOverview from './BGroupProjectOverview'; import completeProjectPropTypes from '../Project/propTypes'; import { loadProjects } from '../Project/actions'; import AddressSearch from '../../components/AddressSearch/AddressSearch'; @@ -31,7 +33,7 @@ const UTILITY_TYPES = { }; /* eslint-disable no-param-reassign */ -class BGroup extends Component { +export class BGroup extends Component { state = { edit: false, utilityAccountsStatus: {}, @@ -44,11 +46,15 @@ class BGroup extends Component { simulationLoading: false, simulationError: false, buildingIdProject: {}, + projectTypeBreakdown: {}, + projectStatusBreakdown: {}, contactAccounts: {}, contactParentAccounts: {}, contactNames: {}, contactLoading: false, contactError: false, + numRows: -1, + overviewTab: this.props.user.permissions['view::bgroupProjectsSummary'] ? 'projects' : 'buildings', } componentDidMount() { @@ -65,9 +71,12 @@ class BGroup extends Component { val.building_id )); if (buildingIds.length > 0) { + this.setState({ numRows: buildingIds.length }); this.props.loadProjects({ 'building_id[]': buildingIds }); this.getUtilityAccounts(buildingIds); - this.getGatewayDates(buildingIds); + if (this.props.user.permissions['read::Gateway']) { + this.getGatewayDates(buildingIds); + } this.getSimulationDates(buildingIds); } } @@ -75,6 +84,8 @@ class BGroup extends Component { nextProps.projects.projects.length > 0 && nextProps.projects.projects !== this.props.projects.project ) { + const projectTypeBreakdown = {}; + const projectStatusBreakdown = {}; const buildingIdProject = nextProps.projects.projects.reduce( (acc, val) => { if (acc[val.building_id]) { @@ -82,14 +93,30 @@ class BGroup extends Component { } else { acc[val.building_id] = [val]; } + if (!(val.project_type in projectTypeBreakdown)) { + projectTypeBreakdown[val.project_type] = 0; + } + if (!(val.project_stage in projectStatusBreakdown)) { + projectStatusBreakdown[val.project_stage] = 0; + } + projectTypeBreakdown[val.project_type] += 1; + projectStatusBreakdown[val.project_stage] += 1; return acc; }, {} ); - this.setState({ buildingIdProject }); + this.setState({ + buildingIdProject, + projectTypeBreakdown, + projectStatusBreakdown, + }); this.getContacts(nextProps.projects.projects); } } + componentWillUnmount() { + clearTimeout(this.updateNumRows); + } + getContacts = (projects) => { // A function to get all of the contact information for buildings this.setState({ contactLoading: true }); @@ -240,6 +267,9 @@ class BGroup extends Component { } renderEditButton = () => { + if (!this.props.user.permissions['delete::BGroup'] && !this.props.user.permissions['update:BGroup']) { + return null; + } if (this.state.edit) { return (
@@ -260,6 +290,9 @@ class BGroup extends Component { ); } + renderProjectSummary = () => { + } + renderDeleteBuildingButton = (id) => { if (this.props.bGroup.deleteBGroupBuildingLoading[id]) { return ( @@ -277,7 +310,7 @@ class BGroup extends Component { ); } - render() { + renderBuildingGroup = () => { let { bGroupBuildings } = this.props.bGroup; // A generic filter method that ensures that empty rows return false const genericFilterMethod = (filter, row) => ( @@ -292,7 +325,7 @@ class BGroup extends Component { if (filter.value === 'all') { return true; } - if (filter.value === 'submitted') { + if (filter.value === 'received') { return row[filter.id]; } return !row[filter.id]; @@ -305,8 +338,8 @@ class BGroup extends Component { value={filter ? filter.value : 'all'} > - - + + ); const columns = [{ @@ -356,11 +389,11 @@ class BGroup extends Component { ), columns: [ { - Header: 'Account', + Header: 'Owner', filterMethod: genericFilterMethod, accessor: 'contact_account', }, { - Header: 'Parent Account', + Header: 'Parent Company', filterMethod: genericFilterMethod, accessor: 'contact_parent_account', }, { @@ -418,7 +451,7 @@ class BGroup extends Component { if (filter.value === 'all') { return true; } - if (filter.value === 'ran') { + if (filter.value === 'complete') { return row[filter.id]; } return !row[filter.id]; @@ -430,8 +463,8 @@ class BGroup extends Component { value={filter ? filter.value : 'all'} > - - + + ), accessor: 'building_simulation', @@ -451,7 +484,8 @@ class BGroup extends Component { ), columns: [ - { + // Only show the sensor install column if the user has the correct permission + ...(!this.props.user.permissions['read::Gateway']) ? [] : [{ Header: 'Sensor Install', filterMethod: (filter, row) => { if (filter.value === 'all') { @@ -496,7 +530,7 @@ class BGroup extends Component { ))} ) : null), - }, { + }], { Header: '# Projects', filterable: false, style: { textAlign: 'center' }, @@ -529,6 +563,7 @@ class BGroup extends Component { if (this.state.edit) { columns.push({ Header: '', + filterable: false, accessor: 'delete', maxWidth: 35, Cell: row => this.renderDeleteBuildingButton(row.original.id), @@ -555,87 +590,163 @@ class BGroup extends Component { val.contact_names = this.state.contactNames[val.building_id]; return val; }); - return ( -
- -
- View all groups -
-
-

{this.props.bGroup.bGroupDetail.name}

- {this.renderEditButton()} -
+
+
+
+ +
-
-
- - -
+
+
= 0 ? '' : 'none' }}> +
+
{this.state.numRows} buildings
-
-
- { - const addresses = bGroupBuildings - .filter(i => i.building_id === row.original.building_id) - .reduce((acc, val) => val.address_list, '') - .split(','); - return ( -
- -
+
+
+
+ { this.reactTable = reactTable; }} + onFilteredChange={() => { + // Create a bounce function so as to not decrease the speed of the filter. + clearTimeout(this.updateNumRows); + this.updateNumRows = setTimeout( + () => { + this.setState({ + numRows: this.reactTable.state.sortedData.length, + }); + }, + 100, + ); + }} + noDataText={this.props.bGroup.bGroupDetailLoading ? 'Loading...' : 'No data'} + SubComponent={row => { + const addresses = bGroupBuildings + .filter(i => i.building_id === row.original.building_id) + .reduce((acc, val) => val.address_list, '') + .split(','); + return ( +
+ +
+
+

+ Address list{' '} + + + +

+
    + {addresses.map(i =>
  • {i}
  • )} +
+
+ {this.state.buildingIdProject[row.original.building_id] ? (

- Address list{' '} - - - + Project list

    - {addresses.map(i =>
  • {i}
  • )} + {this.state.buildingIdProject[row.original.building_id].map((i) => { + let spanStatus = 'pending'; + if (i.state === 'constructed') { + spanStatus = 'claimed'; + } else if (i.state === 'cancelled') { + spanStatus = 'rejected'; + } + return ( +
  • + { i.state } + {' '} - { i.project_type } + {' '} - {i.name} +
  • + ); + })}
- {this.state.buildingIdProject[row.original.building_id] ? ( -
-

- Project list -

-
    - {this.state.buildingIdProject[row.original.building_id].map((i) => { - let spanStatus = 'pending'; - if (i.state === 'constructed') { - spanStatus = 'claimed'; - } else if (i.state === 'cancelled') { - spanStatus = 'rejected'; - } - return ( -
  • - { i.state } - {' '} - { i.project_type } - {' '} - {i.name} -
  • - ); - })} -
-
- ) : null} -
+ ) : null}
- ); - }} - /> +
+ ); + }} + /> +
+
+
+ ); + } + + render() { + let content = ( + + ); + if (!this.props.bGroup.bGroupDetailLoading) { + switch (this.state.overviewTab) { + case 'projects': + content = ( + + ); + break; + case 'buildings': + content = this.renderBuildingGroup(); + break; + default: + content = 'There was an error'; + } + } + + return ( +
+ +
+
+
+
+ View all groups + {' '} +

+ {this.props.bGroup.bGroupDetail.name} +

+
+ {this.renderEditButton()}
+ + {content}
); diff --git a/src/containers/BGroup/BGroup.test.js b/src/containers/BGroup/BGroup.test.js new file mode 100644 index 0000000000000000000000000000000000000000..1c1cb8e6223487f9f40b0fd2c0c9f3712cf8eeaf --- /dev/null +++ b/src/containers/BGroup/BGroup.test.js @@ -0,0 +1,45 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { BGroup } from './BGroup'; +import { initState as bGroupState } from './reducer'; +import { initState as projectState } from '../Project/reducer'; +import { initState as userState } from '../User/reducer'; + +class LocalStorageMock { + constructor() { + this.store = {}; + } + + clear() { + this.store = {}; + } + + getItem(key) { + return this.store[key] || null; + } + + setItem(key, value) { + this.store[key] = value.toString(); + } + + removeItem(key) { + delete this.store[key]; + } +} + +global.localStorage = new LocalStorageMock; // eslint-disable-line + +it('renders without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render( + {}} + loadBGroupDetail={() => {}} + params={{ bGroupId: '' }} + />, + div, + ); +}); diff --git a/src/containers/BGroup/BGroupProjectOverview.js b/src/containers/BGroup/BGroupProjectOverview.js new file mode 100644 index 0000000000000000000000000000000000000000..eccb35e93e675df3d1bcf7d6a1e9c003b864f257 --- /dev/null +++ b/src/containers/BGroup/BGroupProjectOverview.js @@ -0,0 +1,96 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import userPropTypes from '../User/propTypes'; +import completeProjectPropTypes from '../Project/propTypes'; +import Loading from '../../components/Loading'; + +export default class BGroupProjectOverview extends Component { + state = { }; + + render() { + const { + user, projects, + projectStatusBreakdown, + projectTypeBreakdown, + } = this.props; + if (projects.loading) { + return ; + } + if ( + !user.permissions['view::bgroupProjectsSummary'] || + Object.keys(projectTypeBreakdown).length === 0 + ) { + return null; + } + let numProjects = 0; + const stageOrder = ['Engaged', 'Out to Bid', 'In Construction', 'Complete', 'HPD Pipeline']; + return ( +
+
+
+

Project Type Summary

+ + + + + + + + + { + Object.keys(projectTypeBreakdown).sort().map((key) => { + if (key === 'null') { + return null; + } + numProjects += projectTypeBreakdown[key]; + return ( + + + + + ); + }) + } + + + + + +
Project TypeCount
{key}{projectTypeBreakdown[key]}
Total{numProjects}
+
+
+

Project Stage Summary

+ + + + + + + + + { + stageOrder.map((key) => { + return ( + + + + + ); + }) + } + +
Project StageCount
{key}{projectStatusBreakdown[key]}
+
+
+
+ ); + } +} + +BGroupProjectOverview.propTypes = { + user: userPropTypes, + projects: completeProjectPropTypes, + projectTypeBreakdown: PropTypes.object, // eslint-disable-line + projectStatusBreakdown: PropTypes.object, // eslint-disable-line +}; + diff --git a/src/containers/BGroup/reducer.js b/src/containers/BGroup/reducer.js index 30dc95dc4f430def7242f590c43ec655b4dd1553..7bf81845507a08df3693d58b60247d1c66cb84a9 100644 --- a/src/containers/BGroup/reducer.js +++ b/src/containers/BGroup/reducer.js @@ -22,7 +22,7 @@ import { DELETE_BGROUP_BUILDING_FAILED, } from './constants'; -const initState = { +export const initState = { // bgroup bGroupLoading: false, bGroupError: false, diff --git a/src/containers/Project/reducer.js b/src/containers/Project/reducer.js index 875ca7c8040b922aeaa775efdfebdc139c89727c..10ee4934b0874f8cdc5880ec681cc3e34e033e4c 100644 --- a/src/containers/Project/reducer.js +++ b/src/containers/Project/reducer.js @@ -11,7 +11,7 @@ import { FILTER_PROJECTS_FAILURE, } from './constants'; -const initState = { +export const initState = { type: 'name', term: '', projects: [], diff --git a/src/containers/User/reducer.js b/src/containers/User/reducer.js index 9bb411fb3447a70a1532eb36b637eb62c341afcd..7715ba0398cb9916b677fce149fa70b571662742 100644 --- a/src/containers/User/reducer.js +++ b/src/containers/User/reducer.js @@ -19,7 +19,7 @@ const initUser = { permissions: {}, }; -const initState = { +export const initState = { ...initUser, loading: false, error: false,