diff --git a/.env.default b/.env.default index 9460ac939f00840be8696a94c26d678b3f130df3..a31cc652a291d2fc8b70182ec96e9ba96c0fe8ec 100644 --- a/.env.default +++ b/.env.default @@ -25,3 +25,6 @@ REACT_APP_AUTH0_CLIENT_ID REACT_APP_AUTH0_CALLBACK_URL REACT_APP_AUTH0_CLAIMS_NAMESPACE REACT_APP_AUTH0_AUDIENCE + +# BlocPower Building Group ID +REACT_APP_BP_BGROUP_ID diff --git a/README.md b/README.md index ee44ae3af9be9d6b800760de7da178c5ed829f4a..4cba54a1e6e52985f7202cb655547ef101ea107f 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ REACT_APP_AUTH0_CALLBACK_URL REACT_APP_AUTH0_CLAIMS_NAMESPACE REACT_APP_AUTH0_AUDIENCE REACT_APP_FEEDBACK_LINK +REACT_APP_BP_BGROUP_ID ``` ### Optional - [React Devtools](https://github.com/facebook/react-devtools) diff --git a/src/containers/BGroup/BGroup.js b/src/containers/BGroup/BGroup.js index ac3060f9d2c11d347283da1cb3ae7ee2efb295a3..458b4f417d21a70960f130150895c5952d56e499 100644 --- a/src/containers/BGroup/BGroup.js +++ b/src/containers/BGroup/BGroup.js @@ -22,9 +22,14 @@ import BGroupProjectOverview from './BGroupProjectOverview'; import BGroupBuildingTable from './BGroupBuildingTable'; import completeProjectPropTypes from '../Project/propTypes'; import { loadProjects } from '../Project/actions'; +import { loadCustomBpImpactReport } from '../Reports/actions'; +import reportsPropTypes from '../Reports/propTypes'; import userPropType from '../User/propTypes'; import request from '../../utils/request'; -import { getHeaders, accountURL, gatewayURL, contactsURL, sfBuildingImpactURL } from '../../utils/restServices'; +import { + getHeaders, accountURL, gatewayURL, + contactsURL, sfBuildingImpactURL, +} from '../../utils/restServices'; import './styles.css'; const UTILITY_TYPES = { @@ -80,12 +85,10 @@ export class BGroup extends Component { } this.getSimulationDates(buildingIds); this.getSfBuildingImpact(buildingIds); + this.props.loadCustomBpImpactReport(buildingIds); } } - if ( - nextProps.projects.projects.length > 0 && - nextProps.projects.projects !== this.props.projects.project - ) { + if (nextProps.projects.projects !== this.props.projects.project) { const projectTypeBreakdown = {}; const projectStatusBreakdown = {}; const buildingIdProject = nextProps.projects.projects.reduce( @@ -115,8 +118,24 @@ export class BGroup extends Component { } } - componentWillUnmount() { - clearTimeout(this.updateNumRows); + getSfBuildingImpact = (buildingIds) => { + // A function to get impat data for each building + this.setState({ impactLoading: true }); + const filterString = buildingIds.reduce((acc, val) => ( + `${acc}building_id[]=${val}&` + ), ''); + request(`${sfBuildingImpactURL}?${filterString}`, { + method: 'GET', + headers: getHeaders(), + }).then((res) => { + this.setState({ impactLoading: false }); + const data = res.data; + if (!res.err && data) { + this.setState({ impact: data }); + } else { + this.setState({ impactError: true }); + } + }); } getSfBuildingImpact = (buildingIds) => { @@ -298,7 +317,7 @@ export class BGroup extends Component { let content = ( ); - if (!this.props.bGroup.bGroupDetailLoading) { + if (!this.props.bGroup.bGroupDetailLoading && this.props.bGroup.bGroupDetail.name !== '') { switch (this.state.overviewTab) { case 'projects': content = ( @@ -307,6 +326,8 @@ export class BGroup extends Component { projects={this.props.projects} projectStatusBreakdown={this.state.projectStatusBreakdown} projectTypeBreakdown={this.state.projectTypeBreakdown} + impactSummary={this.props.report.customBpImpactReport} + impactSummaryLoading={this.props.report.loadingCustomBpImpact} /> ); break; @@ -336,6 +357,7 @@ export class BGroup extends Component { toggleAnalysis={this.props.toggleAnalysis} toggleProjects={this.props.toggleProjects} toggleImpact={this.props.toggleImpact} + onBuildingFilter={this.props.onBuildingFilter} /> ); break; @@ -348,7 +370,7 @@ export class BGroup extends Component {
{this.props.displayNavBar ? : null}
-
+
View all groups @@ -416,6 +438,8 @@ BGroup.propTypes = { deleteBuildingFromBGroup: PropTypes.func, user: userPropType, projects: completeProjectPropTypes, + report: reportsPropTypes, + loadCustomBpImpactReport: PropTypes.func, loadProjects: PropTypes.func, displayNavBar: PropTypes.bool, displayProjectOverview: PropTypes.bool, @@ -428,6 +452,7 @@ BGroup.propTypes = { toggleAnalysis: PropTypes.bool, toggleProjects: PropTypes.bool, toggleImpact: PropTypes.bool, + onBuildingFilter: PropTypes.func, }; BGroup.defaultProps = { @@ -442,12 +467,14 @@ BGroup.defaultProps = { toggleAnalysis: true, toggleProjects: true, toggleImpact: true, + onBuildingFilter: () => { }, }; const mapStateToProps = state => ( { bGroup: state.bGroup, projects: state.projectList, + report: state.report, } ); @@ -459,6 +486,7 @@ const mapDispatchToProps = dispatch => ( deleteBuildingFromBGroup, deleteBGroup, loadProjects, + loadCustomBpImpactReport, }, dispatch) ); diff --git a/src/containers/BGroup/BGroupBuildingTable.js b/src/containers/BGroup/BGroupBuildingTable.js index 0dec97fb0a85e9c14368eba476b193323680970a..ab26d832f8e74733c3ce2054e5ec2ec0a89bcacf 100644 --- a/src/containers/BGroup/BGroupBuildingTable.js +++ b/src/containers/BGroup/BGroupBuildingTable.js @@ -29,6 +29,10 @@ export default class BGroupBuildingTable extends Component { completedDeemedToggle: {}, }; + componentWillUnmount() { + clearTimeout(this.updateNumRows); + } + handleAddBuilding = (item) => { this.props.addBuildingToBGroup( this.props.bGroupId, @@ -667,6 +671,7 @@ export default class BGroupBuildingTable extends Component { }, 250, ); + this.props.onBuildingFilter(filter, this.reactTable.state); }} noDataText={this.props.bGroup.bGroupDetailLoading ? 'Loading...' : 'No data'} SubComponent={row => { @@ -750,4 +755,5 @@ BGroupBuildingTable.propTypes = { toggleAnalysis: PropTypes.bool, toggleProjects: PropTypes.bool, toggleImpact: PropTypes.bool, + onBuildingFilter: PropTypes.func, }; diff --git a/src/containers/BGroup/BGroupProjectOverview.js b/src/containers/BGroup/BGroupProjectOverview.js index eccb35e93e675df3d1bcf7d6a1e9c003b864f257..cb9e4933f7bfa28a64d15d7a9aa322ceed3601d1 100644 --- a/src/containers/BGroup/BGroupProjectOverview.js +++ b/src/containers/BGroup/BGroupProjectOverview.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import userPropTypes from '../User/propTypes'; import completeProjectPropTypes from '../Project/propTypes'; import Loading from '../../components/Loading'; +import BpImpact from '../../containers/Reports/components/BpImpact/BpImpact'; export default class BGroupProjectOverview extends Component { state = { }; @@ -12,21 +13,31 @@ export default class BGroupProjectOverview extends Component { user, projects, projectStatusBreakdown, projectTypeBreakdown, + impactSummary, + impactSummaryLoading, } = this.props; - if (projects.loading) { + if (projects.loading || impactSummaryLoading) { return ; } - if ( - !user.permissions['view::bgroupProjectsSummary'] || - Object.keys(projectTypeBreakdown).length === 0 - ) { - return null; + if (!user.permissions['view::bgroupProjectsSummary']) { + return
Incorrect permissions
; + } + if (Object.keys(projectTypeBreakdown).length === 0) { + return
Insufficient project data
; } let numProjects = 0; const stageOrder = ['Engaged', 'Out to Bid', 'In Construction', 'Complete', 'HPD Pipeline']; return (
+
+

Project Impact Summary

+ +
+
+

Project Type Summary

@@ -92,5 +103,7 @@ BGroupProjectOverview.propTypes = { projects: completeProjectPropTypes, projectTypeBreakdown: PropTypes.object, // eslint-disable-line projectStatusBreakdown: PropTypes.object, // eslint-disable-line + impactSummary: PropTypes.array, // eslint-disable-line + impactSummaryLoading: PropTypes.bool, }; diff --git a/src/containers/Reports/actions.js b/src/containers/Reports/actions.js index e9ad84e8a2197088748da61f39da8ef8814b4f0c..8941c6052ec5a7f5d7fb8f40264bf99b5ac41757 100644 --- a/src/containers/Reports/actions.js +++ b/src/containers/Reports/actions.js @@ -3,6 +3,12 @@ import { LOAD_LIGHTING_REPORT, LOAD_LIGHTING_REPORT_SUCCESS, LOAD_LIGHTING_REPORT_ERROR, + LOAD_BP_IMPACT_REPORT, + LOAD_BP_IMPACT_REPORT_SUCCESS, + LOAD_BP_IMPACT_REPORT_ERROR, + LOAD_CUSTOM_BP_IMPACT_REPORT, + LOAD_CUSTOM_BP_IMPACT_REPORT_SUCCESS, + LOAD_CUSTOM_BP_IMPACT_REPORT_ERROR, } from './constants'; /** @@ -32,3 +38,57 @@ export function lightingReportsLoadingError(error) { error, }; } + +/** + * Load bp impact reports + * + * @returns {object} An action object with a type of LOAD_BUILDING_BP_IMPACT_REPORT + */ + +export function loadBpImpactReports(limit = -1) { + return { + type: LOAD_BP_IMPACT_REPORT, + limit, + }; +} + +export function bpImpactReportsLoaded(bpImpactReports) { + return { + type: LOAD_BP_IMPACT_REPORT_SUCCESS, + payload: bpImpactReports.data, + }; +} + +export function bpImpactReportsLoadingError(error) { + return { + type: LOAD_BP_IMPACT_REPORT_ERROR, + error, + }; +} + +/** + * Load custom bp impact reports + * + * @returns {object} An action object with a type of LOAD_CUSTOM_BP_IMPACT_REPORT + */ + +export function loadCustomBpImpactReport(buildingIds) { + return { + type: LOAD_CUSTOM_BP_IMPACT_REPORT, + buildingIds, + }; +} + +export function customBpImpactReportsLoaded(bpImpactReport) { + return { + type: LOAD_CUSTOM_BP_IMPACT_REPORT_SUCCESS, + payload: bpImpactReport.data, + }; +} + +export function customBpImpactReportsLoadingError(error) { + return { + type: LOAD_CUSTOM_BP_IMPACT_REPORT_ERROR, + error, + }; +} diff --git a/src/containers/Reports/components/BpImpact/BpImpact.js b/src/containers/Reports/components/BpImpact/BpImpact.js new file mode 100644 index 0000000000000000000000000000000000000000..45d60702b2f6868e2e68b894b74cf65eddc49425 --- /dev/null +++ b/src/containers/Reports/components/BpImpact/BpImpact.js @@ -0,0 +1,81 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import ReactTooltip from 'react-tooltip'; + +import { bpImpactReportPropTypes } from '../../propTypes'; +import Loading from '../../../../components/Loading'; + + +export default class BpImpact extends Component { + + state = {} + + render() { + if (this.props.loading) { + return ; + } + const bpImpactReports = this.props.bpImpactReports; + if (!this.props.bpImpactReports) { + return null; + } + const dateCompareString = ` + The blue number represents the difference between the last two generated reports +
+ The last reports were generated on + ${bpImpactReports[0].created} and ${bpImpactReports.length > 1 ? bpImpactReports[1].created : 'n/a'} + `; + return ( +
+ +
+ + { + bpImpactReports[0].report.map((val, index) => { + let change = 0; + if (bpImpactReports.length > 1) { + const previousReport = bpImpactReports[1]; + change = val.value - previousReport.report[index].value; + } + let changeString = ''; + if (change > 0) { + changeString = ` +${change}`; + } + if (change < 0) { + changeString = ` ${change}`; + } + const changeEl = ( + + {changeString} + + ); + let trStyling = {}; + if (val.col === 'Total Buildings') { + trStyling = { + backgroundColor: 'rgba(176, 167, 167, 0.41)', + pointerEvents: 'none', + }; + } + return ( + + + + + ); + }) + } + +
{val.col}{val.value}{changeEl}
+
+ ); + + } +} + +BpImpact.propTypes = { + bpImpactReports: bpImpactReportPropTypes, + loading: PropTypes.bool, +}; diff --git a/src/containers/Reports/components/BpImpact/BpImpactWrapper.js b/src/containers/Reports/components/BpImpact/BpImpactWrapper.js new file mode 100644 index 0000000000000000000000000000000000000000..eb48e2d469dfa93a1bddaa1c9cdec9d9e47f15f6 --- /dev/null +++ b/src/containers/Reports/components/BpImpact/BpImpactWrapper.js @@ -0,0 +1,122 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import ReactTooltip from 'react-tooltip'; +import { Icon } from 'react-fa'; + +import { bpImpactReportPropTypes } from '../../propTypes'; +import { BGroup } from '../../../../containers/BGroup'; +import userPropType from '../../../../containers/User/propTypes'; +import BpImpact from './BpImpact'; + + +export default class BpImpactWrapper extends Component { + + state = { + filterReport: null, + } + + componentWillReceiveProps(nextProps) { + if ( + nextProps.customBpImpactReport !== this.props.customBpImpactReport + ) { + this.setState({ filterReport: [nextProps.customBpImpactReport[0]] }); + } + } + + componentWillUnmount() { + clearTimeout(this.updateFilterReportTimeout); + } + + updateFilterReport = (filter, tableState) => { + const buildingIds = tableState.sortedData.map(val => ( + val._original.building_id // eslint-disable-line + )); + clearTimeout(this.updateFilterReportTimeout); + this.updateFilterReportTimeout = setTimeout( + () => { + this.props.loadCustomBpImpactReport(buildingIds); + }, + 250, + ); + } + + render() { + if (this.props.bpImpactReports !== null && this.props.bpImpactReports.length < 1) { + return
No report
; + } + if (!this.props.detail) { + return ( + + ); + } + const filterDescriptionString = ` + Filter buildings below to see a new report generated +
+ for the subset of buildings that pass the filter + `; + return ( +
+ +
+
+

Overall Report

+ +
+ +
+
+
+
+

BlocPower Buildings

+ +
+
+
+ ); + } +} + +BpImpactWrapper.propTypes = { + bpImpactReports: bpImpactReportPropTypes, + loadCustomBpImpactReport: PropTypes.func, + customBpImpactReport: PropTypes.array, // eslint-disable-line + loadingCustomBpImpact: PropTypes.bool, + detail: PropTypes.bool, + loading: PropTypes.bool, + user: userPropType, +}; diff --git a/src/containers/Reports/components/Lighting/index.js b/src/containers/Reports/components/Lighting/index.js index af7c884034fcfdca994a4b80fd406af53bf904cb..aa378975d67e57b0abe3389e76cc33733da44928 100644 --- a/src/containers/Reports/components/Lighting/index.js +++ b/src/containers/Reports/components/Lighting/index.js @@ -1,14 +1,19 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Icon } from 'react-fa'; import ReactTooltip from 'react-tooltip'; +import { Icon } from 'react-fa'; import { lightingReportPropTypes } from '../../propTypes'; +import Loading from '../../../../components/Loading'; export default function Lighting({ lightingReports, detail, + loading, }) { + if (loading) { + return ; + } const dateCompareString = ` The blue number represents the difference between the last two generated reports
@@ -76,4 +81,5 @@ export default function Lighting({ Lighting.propTypes = { lightingReports: lightingReportPropTypes, detail: PropTypes.bool, + loading: PropTypes.bool, }; diff --git a/src/containers/Reports/constants.js b/src/containers/Reports/constants.js index 470dc388217739b0d78224e975059ea9c87c9228..1ba142cbde158c06149636f516be5b370175322b 100644 --- a/src/containers/Reports/constants.js +++ b/src/containers/Reports/constants.js @@ -1,3 +1,10 @@ export const LOAD_LIGHTING_REPORT = 'LOAD_LIGHTING_REPORT'; export const LOAD_LIGHTING_REPORT_SUCCESS = 'LOAD_LIGHTING_REPORT_SUCCESS'; export const LOAD_LIGHTING_REPORT_ERROR = 'LOAD_LIGHTING_REPORT_ERROR'; +export const LOAD_BP_IMPACT_REPORT = 'LOAD_BP_IMPACT_REPORT'; +export const LOAD_BP_IMPACT_REPORT_SUCCESS = 'LOAD_BP_IMPACT_REPORT_SUCCESS'; +export const LOAD_BP_IMPACT_REPORT_ERROR = 'LOAD_BP_IMPACT_REPORT_ERROR'; +// A bp impact report for a subset of buildings +export const LOAD_CUSTOM_BP_IMPACT_REPORT = 'LOAD_CUSTOM_BP_IMPACT_REPORT'; +export const LOAD_CUSTOM_BP_IMPACT_REPORT_SUCCESS = 'LOAD_CUSTOM_BP_IMPACT_REPORT_SUCCESS'; +export const LOAD_CUSTOM_BP_IMPACT_REPORT_ERROR = 'LOAD_CUSTOM_BP_IMPACT_REPORT_ERROR'; diff --git a/src/containers/Reports/index.js b/src/containers/Reports/index.js index bf44b61c37c737ef3e574ad618025f4196714bf1..d45cd6f994aad9de82bdf93dd1919391a97ad8bd 100644 --- a/src/containers/Reports/index.js +++ b/src/containers/Reports/index.js @@ -5,13 +5,15 @@ import { connect } from 'react-redux'; import { Link } from 'react-router'; import { bindActionCreators } from 'redux'; import { Nav, NavItem, NavLink } from 'reactstrap'; -import { Icon } from 'react-fa'; import ReactTooltip from 'react-tooltip'; import { loadLightingReports, + loadBpImpactReports, + loadCustomBpImpactReport, } from './actions'; -import lightingReportsPropTypes from './propTypes'; +import reportsPropTypes from './propTypes'; +import BpImpactWrapper from './components/BpImpact/BpImpactWrapper'; import Lighting from './components/Lighting'; import userPropType from '../../containers/User/propTypes'; @@ -20,7 +22,7 @@ class Reports extends Component { super(props); this.state = { display: true, - reportTab: 'lighting', + reportTab: 'bpimpact', mode: this.props.mode, }; } @@ -33,55 +35,71 @@ class Reports extends Component { this.props.loadLightingReports(2); } } - } - componentWillReceiveProps(nextProps) { - if ( - !this.props.user.permissions['read::KissflowLighting'] && - nextProps.user.permissions['read::KissflowLighting'] - ) { - if (this.state.mode === 'summary') { - this.props.loadLightingReports(2, true); - } else { - this.props.loadLightingReports(2); - } + if (this.props.user.permissions['read::BpImpact']) { + this.props.loadBpImpactReports(2); } } render() { - let content = ( -
- -
- ); - if ( - this.props.report.lightingReports.length !== 0 && - !this.props.report.loadingLighting && - !this.props.report.error - ) { - switch (this.state.reportTab) { - case 'lighting': - content = ( - - ); - break; - default: - content = 'There was an error'; - } + let content = null; + let loading = false; + switch (this.state.reportTab) { + case 'bpimpact': + loading = ( + this.props.report.bpImpactReports === null || + this.props.report.loadingBpImpact + ); + content = ( + + ); + break; + case 'lighting': + loading = ( + this.props.report.lightingReports === null || + this.props.report.loadingLighting + ); + content = ( + + ); + break; + default: + content = 'There was an error'; } if (this.props.report.error) { content = 'There was an error'; } - if (!this.props.user.permissions['read::KissflowLighting']) { + if ( + (this.state.reportTab === 'lighting' && !this.props.user.permissions['read::KissflowLighting']) || + (this.state.reportTab === 'bpimpact' && !this.props.user.permissions['read::BpImpact']) + ) { content = 'You do not have the permission to view this report'; } + return ( // create a simple report that is engaged, out to bid, complete, total active, total inactive -
+