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 */}
+ |
+ Consum. |
+ Costs |
+ % |
+ {/* 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)}
+
+
+
+ );
+ }
+}
+
+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 (
-
+
Account Credentials
-
+
{this.state.accountCreated ? (
@@ -791,7 +792,11 @@ class UtilityLine extends Component {
{this.state.accountCreated && 'Account Summary'}
-
+ {this.state.accountCreated && this.state.disaggregateData.length > 0 &&
+
+ }
@@ -860,15 +865,7 @@ UtilityLine.propTypes = {
total_charge_bill: PropTypes.number,
})
),
- disaggregateData: PropTypes.arrayOf(
- PropTypes.shape({
- bill_from_date: PropTypes.string,
- bill_to_date: PropTypes.string,
- heating: PropTypes.number,
- cooling: PropTypes.number,
- other: PropTypes.number,
- })
- ),
+ disaggregateData: disaggregateDataPropTypes,
accountCreated: PropTypes.bool,
street_address: PropTypes.string,
createAccount: PropTypes.func,
diff --git a/src/components/UtilityLine/propTypes.js b/src/components/UtilityLine/propTypes.js
new file mode 100644
index 0000000000000000000000000000000000000000..54c76cdda08adcad80cf670c2fe519d6668b3951
--- /dev/null
+++ b/src/components/UtilityLine/propTypes.js
@@ -0,0 +1,22 @@
+import PropTypes from 'prop-types';
+
+const { string, number, shape, arrayOf } = PropTypes;
+
+const disaggregatedBill = {
+ bill_from_date: string,
+ bill_to_date: string,
+ heating: number,
+ cooling: number,
+ other: number,
+ cooking: number,
+ dhw: number,
+ lighting: number,
+ miscellaneous: number,
+ plug_load: number,
+};
+
+export const disaggregateDataPropTypes = arrayOf(
+ shape({
+ ...disaggregatedBill,
+ })
+);
diff --git a/src/components/UtilityLine/styles.css b/src/components/UtilityLine/styles.css
index ed697fb5183ff8b58bce8bb384cc022dcac9e851..dd21e3306d957952273c2cb56072baa88a5a2b63 100644
--- a/src/components/UtilityLine/styles.css
+++ b/src/components/UtilityLine/styles.css
@@ -10,3 +10,7 @@ input[type=number]::-webkit-outer-spin-button {
input[type=number] {
-moz-appearance: textfield;
}
+
+.date-picker .react-datepicker-wrapper input {
+ width: 100%;
+}
diff --git a/src/utils/formatting.js b/src/utils/formatting.js
new file mode 100644
index 0000000000000000000000000000000000000000..a2d757ea3e04081e39a262a9b5fdd9fdf4520e30
--- /dev/null
+++ b/src/utils/formatting.js
@@ -0,0 +1,30 @@
+import moment from 'moment';
+
+/**
+ * Convert `snake_case` to `Title case`
+ *
+ * @param {String} str
+ */
+export function toTitleCase(str) {
+ return str.split('_')
+ .map(w => w[0].toUpperCase() + w.substr(1).toLowerCase())
+ .join(' ');
+}
+
+/**
+ * Rounds number to two decimal places
+ *
+ * @param {Number} n
+ */
+export function roundNum(n) {
+ return Number(n.toFixed(2));
+}
+
+/**
+ * Convert string into moment date object
+ *
+ * @param {String} dateStr
+ */
+export function convertStringToDate(dateStr) {
+ return moment(dateStr, 'MM/DD/YYYY');
+}