diff --git a/package-lock.json b/package-lock.json index 721669aa922d57331d953b7cec4b2ebc865e8a06..00fc584ec7a3b04e7a02a30cf9ced3f8906b883b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "buildings", + "name": "Dashboard", "version": "1.15.2", "lockfileVersion": 1, "requires": true, @@ -8011,9 +8011,9 @@ } }, "moment": { - "version": "2.19.3", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.19.3.tgz", - "integrity": "sha1-vbmdJw1tf9p4zA+6zoVeJ/59pp8=" + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", + "integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg==" }, "moment-duration-format": { "version": "1.3.0", @@ -8776,10 +8776,15 @@ "babel-runtime": "6.23.0", "immutable": "3.8.2", "immutable-devtools": "0.0.4", - "moment": "2.19.3", + "moment": "2.20.1", "underscore": "1.8.3" } }, + "popper.js": { + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.12.9.tgz", + "integrity": "sha1-DfvC3/lsRRuzMu3Pz6r1ZtMx1bM=" + }, "portfinder": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.13.tgz", @@ -10486,6 +10491,17 @@ "prop-types": "15.6.0" } }, + "react-datepicker": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-1.2.1.tgz", + "integrity": "sha512-zQT35T2zwuozn29iX3SYivRzO8SAPUyKl6PQDvbGhgnzYJd1+eqQVv1S7Cl3N4MiSFeG9PbycMxXrpYhyK6TWA==", + "requires": { + "classnames": "2.2.5", + "prop-types": "15.6.0", + "react-onclickoutside": "6.7.1", + "react-popper": "0.8.2" + } + }, "react-dev-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-1.0.3.tgz", @@ -10963,6 +10979,11 @@ "prop-types": "15.6.0" } }, + "react-onclickoutside": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz", + "integrity": "sha512-p84kBqGaMoa7VYT0vZ/aOYRfJB+gw34yjpda1Z5KeLflg70HipZOT+MXQenEhdkPAABuE2Astq4zEPdMqUQxcg==" + }, "react-placeholder": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/react-placeholder/-/react-placeholder-1.0.8.tgz", @@ -10971,6 +10992,15 @@ "prop-types": "15.6.0" } }, + "react-popper": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-0.8.2.tgz", + "integrity": "sha512-sL3r9aOG8sw48Vs5EiTZV4EXhEH0eoN9718WoIsb0Lx2H/sAZbVLZrENduXCAhre6cEqSh7mMR5sI1luYkVhYQ==", + "requires": { + "popper.js": "1.12.9", + "prop-types": "15.6.0" + } + }, "react-redux": { "version": "4.4.8", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-4.4.8.tgz", @@ -11093,7 +11123,7 @@ "flexbox-react": "4.4.0", "invariant": "2.2.2", "merge": "1.2.0", - "moment": "2.19.3", + "moment": "2.20.1", "moment-duration-format": "1.3.0", "prop-types": "15.6.0", "underscore": "1.8.3" diff --git a/package.json b/package.json index bf3f1673daed5b7e5cf4423e3d0d1f0b03ad9356..f3773b91ff7861a0739b5864d7d4d4aed56f419e 100644 --- a/package.json +++ b/package.json @@ -74,11 +74,13 @@ "highcharts-3d": "^0.1.2", "leaflet": "^1.1.0", "lodash.debounce": "^4.0.8", + "moment": "^2.20.0", "pondjs": "^0.8.8", "prop-types": "^15.5.10", "react": "^15.6.1", "react-autocomplete": "^1.7.2", "react-copy-to-clipboard": "^5.0.0", + "react-datepicker": "^1.2.1", "react-dom": "^15.3.2", "react-fa": "^4.2.0", "react-ga": "^2.2.0", @@ -111,7 +113,9 @@ "collectCoverageFrom": [ "src/**/*.{js,jsx}" ], - "coverageReporters": ["cobertura"], + "coverageReporters": [ + "cobertura" + ], "coverageDirectory": "coverage", "moduleFileExtensions": [ "jsx", diff --git a/src/components/Utilities/index.js b/src/components/Utilities/index.js index 06c0b18e73bfbf5db18a56909a2b457e26cad6aa..bda57cde60da427fcf2870105e52c1622ac08a6f 100644 --- a/src/components/Utilities/index.js +++ b/src/components/Utilities/index.js @@ -251,11 +251,12 @@ class Utilities extends Component { heating: val.heating_usage, cooling: val.cooling_usage, other: val.other_usage, - dhw: val.dhw, - cooking: val.cooking, - lighting: val.lighting, - plug_load: val.plug_load, - miscellaneous: val.miscellaneous, + dhw: val.dhw || 0, + cooking: val.cooking || 0, + lighting: val.lighting || 0, + plug_load: val.plug_load || 0, + miscellaneous: val.miscellaneous || 0, + unit_price: val.unit_price, })); return disaggregateData; } diff --git a/src/components/UtilityAccountSummary.js b/src/components/UtilityAccountSummary.js deleted file mode 100644 index 35345068a7df92a78f0a6970a809364b3d098c8a..0000000000000000000000000000000000000000 --- a/src/components/UtilityAccountSummary.js +++ /dev/null @@ -1,13 +0,0 @@ -import React, { Component } from 'react'; - -class UtilityAccountSummary extends Component { - state = { } - - render() { - return ( -
Coming Soon
- ); - } -} - -export default UtilityAccountSummary; diff --git a/src/components/UtilityLine/UtilityAccountSummary.js b/src/components/UtilityLine/UtilityAccountSummary.js new file mode 100644 index 0000000000000000000000000000000000000000..6d4c67c4033fcda3606d0a9592d69b927411636d --- /dev/null +++ b/src/components/UtilityLine/UtilityAccountSummary.js @@ -0,0 +1,193 @@ +import React, { Component } from 'react'; +import { Container, Row, Col, Table } from 'reactstrap'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; +import moment from 'moment'; +import { disaggregateDataPropTypes } from './propTypes'; +import { toTitleCase, convertStringToDate } from '../../utils/formatting'; +import './styles.css'; + + +class UtilityAccountSummary extends Component { + constructor(props) { + super(props); + + let startDate = this.getBillStartDate(); + const endDate = this.props.disaggregateData.slice(-1).pop().bill_to_date; + + // set start date 12 billing periods ago + if (this.props.disaggregateData.length >= 12) { + startDate = this.props.disaggregateData.slice(-12, -11).pop().bill_from_date; + } + + this.state = { + startDate: convertStringToDate(startDate), + endDate: convertStringToDate(endDate), + }; + } + + /** + * Retrieves first bill + */ + getBillStartDate = () => { + const firstBill = this.props.disaggregateData[0]; + return convertStringToDate(firstBill.bill_from_date); + } + + getBillDates = (toDates = false) => { + const dates = this.props.disaggregateData.reduce((acc, bill) => { + if (!toDates) { + acc.push(convertStringToDate(bill.bill_from_date)); + } else { + acc.push(convertStringToDate(bill.bill_to_date)); + } + + return acc; + }, []); + + return dates; + } + + /** + * Sums each usage type in the disaggregation data and sums + * all the usage types for a final total + */ + computeDisaggregateTotals = () => { + const filterdDisaggregateData = this.props.disaggregateData.filter((bill) => { + return convertStringToDate(bill.bill_from_date) >= this.state.startDate && + convertStringToDate(bill.bill_to_date) <= this.state.endDate; + }); + + // Sum each usage + let totals = filterdDisaggregateData.reduce((acc, item) => { + Object.keys(item).forEach((key) => { + + if (!key.includes('date') && key !== 'other' && key !== 'unit_price') { + acc[key] = (acc[key] || 0) + item[key]; + acc[`${key}_cost`] = acc[key] * item.unit_price; + acc.total += item[key]; + acc.total_cost += item[key] * item.unit_price; + } + + }); + + return acc; + }, { total: 0, total_cost: 0 }); + + // Round all usages + totals = Object.keys(totals) + .reduce((acc, key) => { + acc[key] = Number(totals[key].toFixed(2)); + return acc; + }, { }); + + return totals; + } + + generateRow = (usage, consumption, cost, percentage) => { + return ( + + {usage} + {Math.floor(consumption)} + ${Math.floor(cost.toFixed(2))} + {Math.floor(percentage * 100)} % + + ); + } + + handleChange = (date, endDate = false) => { + if (!endDate) { + this.setState({ + startDate: date, + }); + } else { + this.setState({ + endDate: date, + }); + } + } + + render() { + if (this.props.disaggregateData.length < 2) { + return
; + } + + const usageTotals = this.computeDisaggregateTotals(); + const sortedUsageKeys = Object.keys(usageTotals).sort(); + + return ( +
+ + + + this.handleChange(date, false)} + selected={this.state.startDate} + style={{ width: '80%' }} + showYearDropdown + scrollableYearDropdown + /> + + + this.handleChange(date, true)} + selected={this.state.endDate} + style={{ width: '80%' }} + showYearDropdown + scrollableYearDropdown + /> + + + + + + + {/* eslint-disable no-use-before-define */} + + + + {/* eslint-enable */} + + + + {sortedUsageKeys.reduce((acc, key) => { + if (usageTotals[key] !== 0 && key !== 'total' && key !== 'other' && !key.includes('cost')) { + acc.push( + this.generateRow( + toTitleCase(key), + usageTotals[key], + usageTotals[`${key}_cost`], + usageTotals[key] / usageTotals.total, + ) + ); + } + return acc; + }, [])} + {this.generateRow('Total', usageTotals.total, usageTotals.total_cost, 1)} + +
+ Consum.Costs%
+
+ ); + } +} + +const styles = { + tableHeading: { + padding: 5, + }, +}; + +UtilityAccountSummary.propTypes = { + disaggregateData: disaggregateDataPropTypes, +}; + +export default UtilityAccountSummary; diff --git a/src/components/UtilityLine/UtilityDisaggregation.js b/src/components/UtilityLine/UtilityDisaggregation.js index 3b324fbcd54ca99a96d0be78e6f0886694aa0077..c2b9c2e2ef3b89a05a4f4a39dbdc93b4f115a623 100644 --- a/src/components/UtilityLine/UtilityDisaggregation.js +++ b/src/components/UtilityLine/UtilityDisaggregation.js @@ -298,7 +298,7 @@ class UtilityDisaggregation extends Component { return (
-
+