diff --git a/package-lock.json b/package-lock.json
index 5b104203572cf6efd2de65959ea7f37be669bbf3..eef4367f6abde6981b22b1eb0945f8708b1b43d4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1771,6 +1771,7 @@
"version": "4.0.0-alpha.6",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.0.0-alpha.6.tgz",
"integrity": "sha1-T1TdM6wN6sOyhAe8LffsYIhpycg=",
+ "dev": true,
"requires": {
"jquery": "3.2.1",
"tether": "1.4.0"
@@ -1829,15 +1830,6 @@
}
}
},
- "bpl": {
- "version": "git+https://7f8bbb4b0a383ad905fee5b0f7cbc0c22533b556:x-oauth-basic@github.com/Blocp/bpl.git#044aab932460e8c5d2cc3f993538a3910d42e660",
- "requires": {
- "bootstrap": "4.0.0-alpha.6",
- "copy-dir": "0.3.0",
- "jquery": "3.2.1",
- "tether": "1.4.0"
- }
- },
"brace-expansion": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
@@ -2159,6 +2151,61 @@
}
}
},
+ "cheerio": {
+ "version": "0.22.0",
+ "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz",
+ "integrity": "sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=",
+ "dev": true,
+ "requires": {
+ "css-select": "1.2.0",
+ "dom-serializer": "0.1.0",
+ "entities": "1.1.1",
+ "htmlparser2": "3.9.2",
+ "lodash.assignin": "4.2.0",
+ "lodash.bind": "4.2.1",
+ "lodash.defaults": "4.2.0",
+ "lodash.filter": "4.6.0",
+ "lodash.flatten": "4.4.0",
+ "lodash.foreach": "4.5.0",
+ "lodash.map": "4.6.0",
+ "lodash.merge": "4.6.1",
+ "lodash.pick": "4.4.0",
+ "lodash.reduce": "4.6.0",
+ "lodash.reject": "4.6.0",
+ "lodash.some": "4.6.0"
+ },
+ "dependencies": {
+ "domhandler": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.1.tgz",
+ "integrity": "sha1-iS5HAAqZvlW783dP/qBWHYh5wlk=",
+ "dev": true,
+ "requires": {
+ "domelementtype": "1.3.0"
+ }
+ },
+ "htmlparser2": {
+ "version": "3.9.2",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz",
+ "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=",
+ "dev": true,
+ "requires": {
+ "domelementtype": "1.3.0",
+ "domhandler": "2.4.1",
+ "domutils": "1.5.1",
+ "entities": "1.1.1",
+ "inherits": "2.0.3",
+ "readable-stream": "2.3.3"
+ }
+ },
+ "lodash.defaults": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
+ "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=",
+ "dev": true
+ }
+ }
+ },
"chokidar": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
@@ -2522,14 +2569,6 @@
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.1.tgz",
"integrity": "sha1-Qa1XsbVVlR7BcUEqgZQrHoIA00o="
},
- "copy-dir": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/copy-dir/-/copy-dir-0.3.0.tgz",
- "integrity": "sha1-3rLcL6nJKQ7UfIQVWpmabUX1o1g=",
- "requires": {
- "mkdir-p": "0.0.7"
- }
- },
"copy-to-clipboard": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz",
@@ -3519,6 +3558,56 @@
"integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=",
"dev": true
},
+ "enzyme": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-2.8.2.tgz",
+ "integrity": "sha1-bIvLBQEqvEqkvDIT+yN4C5tbFxQ=",
+ "dev": true,
+ "requires": {
+ "cheerio": "0.22.0",
+ "function.prototype.name": "1.1.0",
+ "is-subset": "0.1.1",
+ "lodash": "4.17.4",
+ "object-is": "1.0.1",
+ "object.assign": "4.1.0",
+ "object.entries": "1.0.4",
+ "object.values": "1.0.4",
+ "prop-types": "15.6.0",
+ "uuid": "2.0.3"
+ },
+ "dependencies": {
+ "uuid": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz",
+ "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=",
+ "dev": true
+ }
+ }
+ },
+ "enzyme-adapter-react-15": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/enzyme-adapter-react-15/-/enzyme-adapter-react-15-1.0.5.tgz",
+ "integrity": "sha512-GxQ+ZYbo6YFwwpaLc9LLyAwsx+F1au628/+hwTx3XV2OiuvHGyWgC/r1AAK1HlDRjujzfwwMNZTc/JxkjIuYVg==",
+ "dev": true,
+ "requires": {
+ "enzyme-adapter-utils": "1.3.0",
+ "lodash": "4.17.4",
+ "object.assign": "4.1.0",
+ "object.values": "1.0.4",
+ "prop-types": "15.6.0"
+ }
+ },
+ "enzyme-adapter-utils": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/enzyme-adapter-utils/-/enzyme-adapter-utils-1.3.0.tgz",
+ "integrity": "sha512-vVXSt6uDv230DIv+ebCG66T1Pm36Kv+m74L1TrF4kaE7e1V7Q/LcxO0QRkajk5cA6R3uu9wJf5h13wOTezTbjA==",
+ "dev": true,
+ "requires": {
+ "lodash": "4.17.4",
+ "object.assign": "4.1.0",
+ "prop-types": "15.6.0"
+ }
+ },
"errno": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz",
@@ -5409,6 +5498,17 @@
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
+ "function.prototype.name": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.0.tgz",
+ "integrity": "sha512-Bs0VRrTz4ghD8pTmbJQD1mZ8A/mN0ur/jGz+A6FBxPDUPkm1tNfF6bhTYPA7i7aF4lZJVr+OXTNNrnnIl58Wfg==",
+ "dev": true,
+ "requires": {
+ "define-properties": "1.1.2",
+ "function-bind": "1.1.1",
+ "is-callable": "1.1.3"
+ }
+ },
"functional-red-black-tree": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
@@ -5730,6 +5830,12 @@
"integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
"dev": true
},
+ "has-symbols": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz",
+ "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=",
+ "dev": true
+ },
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
@@ -6552,6 +6658,12 @@
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
},
+ "is-subset": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz",
+ "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=",
+ "dev": true
+ },
"is-svg": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-svg/-/is-svg-2.1.0.tgz",
@@ -7032,7 +7144,8 @@
"jquery": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.2.1.tgz",
- "integrity": "sha1-XE2d5lKvbNCncBVKYxu6ErAVx4c="
+ "integrity": "sha1-XE2d5lKvbNCncBVKYxu6ErAVx4c=",
+ "dev": true
},
"js-base64": {
"version": "2.3.2",
@@ -7403,6 +7516,18 @@
"integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=",
"dev": true
},
+ "lodash.assignin": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/lodash.assignin/-/lodash.assignin-4.2.0.tgz",
+ "integrity": "sha1-uo31+4QesKPoBEIysOJjqNxqKKI=",
+ "dev": true
+ },
+ "lodash.bind": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz",
+ "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=",
+ "dev": true
+ },
"lodash.camelcase": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
@@ -7453,11 +7578,29 @@
}
}
},
+ "lodash.filter": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz",
+ "integrity": "sha1-ZosdSYFgOuHMWm+nYBQ+SAtMSs4=",
+ "dev": true
+ },
+ "lodash.flatten": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
+ "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=",
+ "dev": true
+ },
"lodash.flow": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz",
"integrity": "sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o="
},
+ "lodash.foreach": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
+ "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=",
+ "dev": true
+ },
"lodash.isarguments": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
@@ -7491,12 +7634,24 @@
"lodash.isarray": "3.0.4"
}
},
+ "lodash.map": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz",
+ "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=",
+ "dev": true
+ },
"lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
"dev": true
},
+ "lodash.merge": {
+ "version": "4.6.1",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz",
+ "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ==",
+ "dev": true
+ },
"lodash.mergewith": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz",
@@ -7513,12 +7668,36 @@
"resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz",
"integrity": "sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA="
},
+ "lodash.pick": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
+ "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=",
+ "dev": true
+ },
+ "lodash.reduce": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz",
+ "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs=",
+ "dev": true
+ },
+ "lodash.reject": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/lodash.reject/-/lodash.reject-4.6.0.tgz",
+ "integrity": "sha1-gNZJLcFHCGS79YNTO2UfQqn1JBU=",
+ "dev": true
+ },
"lodash.restparam": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz",
"integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=",
"dev": true
},
+ "lodash.some": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz",
+ "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=",
+ "dev": true
+ },
"lodash.template": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz",
@@ -7793,11 +7972,6 @@
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true
},
- "mkdir-p": {
- "version": "0.0.7",
- "resolved": "https://registry.npmjs.org/mkdir-p/-/mkdir-p-0.0.7.tgz",
- "integrity": "sha1-JMXb4m2jqZ7xWKHu+aXC3Z3laDw="
- },
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
@@ -8139,12 +8313,42 @@
"integrity": "sha512-smRWXzkvxw72VquyZ0wggySl7PFUtoDhvhpdwgESXxUrH7vVhhp9asfup1+rVLrhsl7L45Ee1Q/l5R2Ul4MwUg==",
"dev": true
},
+ "object-is": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz",
+ "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=",
+ "dev": true
+ },
"object-keys": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz",
"integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=",
"dev": true
},
+ "object.assign": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
+ "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
+ "dev": true,
+ "requires": {
+ "define-properties": "1.1.2",
+ "function-bind": "1.1.1",
+ "has-symbols": "1.0.0",
+ "object-keys": "1.0.11"
+ }
+ },
+ "object.entries": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.0.4.tgz",
+ "integrity": "sha1-G/mk3SKI9bM/Opk9JXZh8F0WGl8=",
+ "dev": true,
+ "requires": {
+ "define-properties": "1.1.2",
+ "es-abstract": "1.10.0",
+ "function-bind": "1.1.1",
+ "has": "1.0.1"
+ }
+ },
"object.omit": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz",
@@ -8155,6 +8359,18 @@
"is-extendable": "0.1.1"
}
},
+ "object.values": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.0.4.tgz",
+ "integrity": "sha1-5STaCbT2b/Bd9FdUbscqyZ8TBpo=",
+ "dev": true,
+ "requires": {
+ "define-properties": "1.1.2",
+ "es-abstract": "1.10.0",
+ "function-bind": "1.1.1",
+ "has": "1.0.1"
+ }
+ },
"obuf": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.1.tgz",
@@ -10790,6 +11006,33 @@
"classnames": "2.2.5"
}
},
+ "react-test-renderer": {
+ "version": "15.6.1",
+ "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-15.6.1.tgz",
+ "integrity": "sha1-Am9KW7VVJmH9LMS7zQ1LyKNev34=",
+ "dev": true,
+ "requires": {
+ "fbjs": "0.8.16",
+ "object-assign": "4.1.1"
+ },
+ "dependencies": {
+ "fbjs": {
+ "version": "0.8.16",
+ "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz",
+ "integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=",
+ "dev": true,
+ "requires": {
+ "core-js": "1.2.7",
+ "isomorphic-fetch": "2.2.1",
+ "loose-envify": "1.3.1",
+ "object-assign": "4.1.1",
+ "promise": "7.1.1",
+ "setimmediate": "1.0.5",
+ "ua-parser-js": "0.7.17"
+ }
+ }
+ }
+ },
"react-tether": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/react-tether/-/react-tether-0.5.7.tgz",
diff --git a/package.json b/package.json
index a5c9f8c4f2940ebe62e7baa969091b63fd0287ef..a4fcff73f73e7540a64281b382c9a1d0faab1669 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,8 @@
"css-loader": "0.28.1",
"detect-port": "1.0.1",
"dotenv": "4.0.0",
+ "enzyme": "^2.8.2",
+ "enzyme-adapter-react-15": "^1.0.5",
"eslint": "^4.3.0",
"eslint-config-airbnb": "^15.1.0",
"eslint-config-react-app": "^2.0.1",
@@ -49,6 +51,7 @@
"promise": "7.1.1",
"react-dev-utils": "^1.0.0",
"react-error-overlay": "^1.0.0",
+ "react-test-renderer": "^15.6.1",
"recursive-readdir": "2.1.0",
"resolve-url-loader": "1.6.1",
"rimraf": "2.5.4",
@@ -89,6 +92,7 @@
"react-table": "^6.7.4",
"react-timeseries-charts": "^0.12.8",
"react-tooltip": "^3.3.0",
+ "react-transition-group": "^2.2.1",
"reactstrap": "^4.8.0",
"recharts": "^0.21.2",
"redux": "^3.6.0",
diff --git a/src/components/Fade.js b/src/components/Fade.js
new file mode 100644
index 0000000000000000000000000000000000000000..93fb2e417ec59655d14c233096ae5d5d6b835195
--- /dev/null
+++ b/src/components/Fade.js
@@ -0,0 +1,38 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Transition from 'react-transition-group/Transition';
+
+const DURATION = 300;
+
+const Fade = ({ in: inProp, children }) => (
+
+ {(state) => (
+
+ {children}
+
+ )}
+
+);
+
+const styles = {
+ defaultStyle: {
+ transition: `opacity ${DURATION}ms ease-in-out`,
+ opacity: 0,
+ display: 'inline-block',
+ },
+ transitionStyles: {
+ entering: { opacity: 0 },
+ entered: { opacity: 1 },
+ },
+};
+
+Fade.propTypes = {
+ in: PropTypes.bool,
+ children: PropTypes.element,
+};
+
+export default Fade;
diff --git a/src/components/Utilities/index.js b/src/components/Utilities/index.js
index ee93bfb3bd07a7ee31ed2b7f1a395a84aefc7bbb..06c0b18e73bfbf5db18a56909a2b457e26cad6aa 100644
--- a/src/components/Utilities/index.js
+++ b/src/components/Utilities/index.js
@@ -84,6 +84,9 @@ class Utilities extends Component {
action: 'Dissagregate Bill Request',
label: `Utility: ${form.utility}, Account: ${form.account_number}`,
});
+
+ updateLoadingDisaggregateState(true);
+
request(disaggregateURL, {
method: 'POST',
headers: getHeaders(),
@@ -92,30 +95,33 @@ class Utilities extends Component {
account_id: accountId,
building_id: this.props.buildingId,
}),
- }).then((res) => {
- if (res.err) {
+ }).then((disaggregateRes) => {
+ if (disaggregateRes.err) {
ReactGA.event({
category: 'Utilities',
action: 'Disaggregate Bill Error',
label: `Utility: ${form.utility},
Account: ${form.account_number},
- Error: ${res.err}`,
+ Error: ${disaggregateRes.err}`,
});
- this.setState({ error: res.err });
+ this.setState({ error: disaggregateRes.err });
} else {
ReactGA.event({
category: 'Utilities',
action: 'Disaggregate Bill Success',
label: `Utility: ${form.utility}, Account: ${form.account_number}`,
});
- const data = res.data;
+ const data = disaggregateRes.data;
const disaggregateData = this.parseDisaggregateData(data.disaggregate_database_output);
setDisaggregateData(disaggregateData);
setRSquared(data.r_squared);
this.resetErrorMessage();
}
- updateLoadingDisaggregateState(false);
+
+ setTimeout(() => {
+ updateLoadingDisaggregateState(false);
+ }, 1000);
});
}
@@ -245,6 +251,11 @@ 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,
}));
return disaggregateData;
}
diff --git a/src/components/UtilityLine/DisaggregationForm.js b/src/components/UtilityLine/DisaggregationForm.js
new file mode 100644
index 0000000000000000000000000000000000000000..c49f5edb8d4fad40512eb59847db1bf517a6f542
--- /dev/null
+++ b/src/components/UtilityLine/DisaggregationForm.js
@@ -0,0 +1,79 @@
+import React, { Component } from 'react';
+import { Input } from 'reactstrap';
+import { Icon } from 'react-fa';
+import PropTypes from 'prop-types';
+
+
+class DisaggregationForm extends Component {
+ static usageTypes = {
+ 5: 'DHW',
+ 6: 'Lighting',
+ 7: 'Cooking',
+ 8: 'Plug load',
+ }
+
+ render() {
+ return (
+
+ |
+ this.props.handleInputChange(e)}
+ value={this.props.form.usage_type_id}
+ >
+ {this.props.showDelete &&
+
+ }
+ {Object.keys(this.props.availableUsageTypes).map((key) => (
+ )
+ )}
+
+ |
+
+
+ this.props.handleInputChange(e)}
+ value={this.props.form.value}
+ />
+ |
+
+
+ {!this.props.showDelete ?
+ this.props.addEndUsage()} /> :
+ this.props.deleteEndUsage()} />
+ }
+ |
+
+ );
+ }
+}
+
+DisaggregationForm.propTypes = {
+ addEndUsage: PropTypes.func,
+ deleteEndUsage: PropTypes.func,
+ handleInputChange: PropTypes.func,
+ form: PropTypes.shape({
+ id: PropTypes.number,
+ usage_type_id: PropTypes.number,
+ value: PropTypes.number,
+ }),
+ showDelete: PropTypes.bool,
+ availableUsageTypes: PropTypes.object, // eslint-disable-line
+};
+
+DisaggregationForm.defaultProps = {
+ showDelete: false,
+};
+
+export default DisaggregationForm;
diff --git a/src/components/UtilityLine/DisaggregationForm.test.js b/src/components/UtilityLine/DisaggregationForm.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..cfaaaedb032140f20289f22c4b8bb4f146d10251
--- /dev/null
+++ b/src/components/UtilityLine/DisaggregationForm.test.js
@@ -0,0 +1,74 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Table } from 'reactstrap';
+import renderer from 'react-test-renderer';
+import { shallow } from 'enzyme';
+import DisaggregationForm from './DisaggregationForm';
+
+const form = {
+ usage_type_id: 5,
+ value: 99,
+};
+
+const SimpleTable = (props) => (
+
+
+ {
+ // eslint-disable-next-line
+ }{props.children}
+
+
+);
+
+it('renders without crashing', () => {
+ const div = document.createElement('div');
+ ReactDOM.render(
+
+ {}}
+ deleteEndUsage={() => {}}
+ handleInputChange={() => {}}
+ form={form}
+ availableUsageTypes={{ 5: 'DHW' }}
+ />
+ ,
+ div);
+});
+
+test('DisaggregationForm renders form passed in', () => {
+ const shallowDisaggregationForm = shallow(
+ {}}
+ deleteEndUsage={() => {}}
+ handleInputChange={() => {}}
+ form={form}
+ availableUsageTypes={{ 5: 'DHW' }}
+ />
+ );
+
+ // Make sure state is initialized by props
+ // const componentForm = shallowDisaggregationForm.state().form;
+ // expect(componentForm.usage_type_id).toEqual(form.usage_type_id);
+ // expect(componentForm.value).toEqual(form.value);
+ // expect(componentForm.is_percentage).toEqual(form.is_percentage);
+
+ // Make sure form is rendered to DOM
+ expect(shallowDisaggregationForm.find('[name="usage_type_id"]').props().value).toEqual(form.usage_type_id);
+ expect(shallowDisaggregationForm.find('[name="value"]').props().value).toEqual(form.value);
+});
+
+test('DisaggregationForm snapshot with form', () => {
+ const component = renderer.create(
+
+ {}}
+ deleteEndUsage={() => {}}
+ handleInputChange={() => {}}
+ form={form}
+ availableUsageTypes={{ 5: 'DHW' }}
+ />
+ ,
+ );
+ let tree = component.toJSON();
+ expect(tree).toMatchSnapshot();
+});
diff --git a/src/components/UtilityLine/UtilityDisaggregation.js b/src/components/UtilityLine/UtilityDisaggregation.js
index a01fe1a5bd194a0659a769ef6e2b0fb9e5647d8b..ce59f31cc95209190d772815f5b5b5339fc1adc0 100644
--- a/src/components/UtilityLine/UtilityDisaggregation.js
+++ b/src/components/UtilityLine/UtilityDisaggregation.js
@@ -1,11 +1,286 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Icon } from 'react-fa';
-import { generateBillDownload } from './dataInteraction';
+import { Table } from 'reactstrap';
+import DisaggregationForm from './DisaggregationForm';
+import Fade from '../Fade';
+import request from '../../utils/request';
+import { getHeaders, disaggregateMetaURL } from '../../utils/restServices';
+import { getDateDiff } from './dataInteraction';
+import ErrorAlert from '../ErrorAlert';
+import Loading from '../Loading';
+import './styles.css';
class UtilityDisaggregation extends Component {
- state = { }
+ /* eslint-disable quote-props */
+ static disaggregateDataMapping = {
+ 'Bill From Date': 'bill_from_date',
+ 'Bill To Date': 'bill_to_date',
+ 'Days In Bill': 'days_in_bill',
+ 'Usage': 'usage',
+ 'Heating Usage': 'heating',
+ 'Cooling Usage': 'cooling',
+ 'Other Usage': 'other',
+ 'DHW': 'dhw',
+ 'Lighting': 'lighting',
+ 'Cooking': 'cooking',
+ 'Plug Load': 'plug_load',
+ 'Miscellaneous': 'miscellaneous',
+ }
+ /* eslint-enable quote-props */
+
+ static usageTypes = {
+ 5: 'DHW',
+ 6: 'Lighting',
+ 7: 'Cooking',
+ 8: 'Plug load',
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ availableUsageTypes: this.determineAvailableUsageTypes([]),
+ endUsages: [],
+ endUsagesToDelete: [],
+ editMode: false,
+ fetchError: null,
+ loadingDisaggregateMeta: false,
+ saveError: null,
+ savingDisaggregateMeta: false,
+ pendingEndUsage: this.resetPendingEndUsage({ ...UtilityDisaggregation.usageTypes }),
+ };
+ }
+
+ componentDidMount() {
+ if (this.props.accountId === null) {
+ return;
+ }
+
+ // eslint-disable-next-line
+ this.setState({ loadingDisaggregateMeta: true });
+
+ request(`${disaggregateMetaURL}?account_id=${this.props.accountId}`, {
+ method: 'GET',
+ headers: getHeaders(),
+ }).then((res) => {
+ if (!res.err) {
+ const newAvailableUsageTypes = this.determineAvailableUsageTypes(res.data);
+
+ this.setState({
+ endUsages: res.data,
+ availableUsageTypes: newAvailableUsageTypes,
+ loadingDisaggregateMeta: false,
+ pendingEndUsage: this.resetPendingEndUsage(newAvailableUsageTypes),
+ });
+ } else {
+ this.setState({
+ fetchError: res.err,
+ loadingDisaggregateMeta: false,
+ });
+ }
+ });
+ }
+
+ toggleEditMode = () => {
+ if (this.state.editMode) {
+ this.saveDisaggregateMeta();
+ }
+
+ this.setState({ editMode: !this.state.editMode });
+ }
+
+ resetPendingEndUsage = (availableUsageTypes) => {
+ return {
+ id: null,
+ usage_type_id: Number(Object.keys(availableUsageTypes)[0]),
+ value: 0,
+ };
+ }
+
+ calculateMiscEndUsage = () => {
+ const total = 100 - this.state.endUsages.reduce((acc, i) => acc + i.value, 0);
+ return { total, overLimit: total < 0 };
+ }
+
+ addNewEndUsage = () => {
+ const updateEndUsages = [
+ ...this.state.endUsages,
+ this.state.pendingEndUsage,
+ ];
+
+ const updateUsageTypes = this.determineAvailableUsageTypes(updateEndUsages);
+
+ this.setState({
+ endUsages: updateEndUsages,
+ availableUsageTypes: updateUsageTypes,
+ pendingEndUsage: this.resetPendingEndUsage(updateUsageTypes),
+ });
+ }
+
+ deleteEndUsage = (index) => {
+ const endUsageCopy = [...this.state.endUsages];
+ const toBeDeleted = endUsageCopy.splice(index, 1).pop();
+
+ const confirmAns = confirm('Are you sure you want to delete this?');
+ if (!confirmAns) {
+ return;
+ }
+
+ if (toBeDeleted.id != null) {
+ this.setState({
+ endUsagesToDelete: [...this.state.endUsagesToDelete, toBeDeleted],
+ });
+ }
+
+ const newAvailableUsageTypes = this.determineAvailableUsageTypes(endUsageCopy);
+
+ this.setState({
+ endUsages: endUsageCopy,
+ availableUsageTypes: newAvailableUsageTypes,
+ pendingEndUsage: this.resetPendingEndUsage(newAvailableUsageTypes),
+ });
+ }
+
+ saveDisaggregateMeta = () => {
+ this.setState({ savingDisaggregateMeta: true });
+
+ const putDisaggregate = request(`${disaggregateMetaURL}bulk/`, {
+ method: 'PUT',
+ headers: getHeaders(),
+ body: JSON.stringify({
+ account_id: this.props.accountId,
+ disaggregateMeta: this.state.endUsages,
+ }),
+ });
+
+ const deleteDisaggregate = request(`${disaggregateMetaURL}bulk/`, {
+ method: 'DELETE',
+ headers: getHeaders(),
+ body: JSON.stringify({
+ ids: this.state.endUsagesToDelete.map(i => i.id),
+ }),
+ });
+
+ Promise.all([putDisaggregate, deleteDisaggregate]).then((values) => {
+ const putDisaggregateRes = values[0];
+ const deleteDisaggregateRes = values[1];
+
+ if (putDisaggregateRes.err) {
+ this.setState({ saveError: putDisaggregateRes.err });
+ return;
+ }
+
+ if (deleteDisaggregateRes.err) {
+ this.setState({ saveError: deleteDisaggregateRes.err });
+ return;
+ }
+
+ const newAvailableUsageTypes = this.determineAvailableUsageTypes(putDisaggregateRes.data);
+
+ this.setState({
+ endUsages: putDisaggregateRes.data,
+ availableUsageTypes: newAvailableUsageTypes,
+ pendingEndUsage: this.resetPendingEndUsage(newAvailableUsageTypes),
+ });
+
+ setTimeout(() => {
+ this.setState({ savingDisaggregateMeta: false });
+ this.props.handleDisaggregateUtilityBill();
+ }, 1000);
+ });
+ }
+
+ determineAvailableUsageTypes = (endUsages) => {
+ const newUsageTypeAvailable = { ...UtilityDisaggregation.usageTypes };
+
+ endUsages.forEach(i => (
+ delete newUsageTypeAvailable[i.usage_type_id]
+ ));
+
+ return newUsageTypeAvailable;
+ }
+
+ handleInputChange = (event, index = null) => {
+ const target = event.target;
+ const name = target.name;
+ let val = null;
+
+ switch (target.type) {
+ case 'checkbox':
+ val = target.checked;
+ break;
+
+ case 'number':
+ case 'select-one':
+ val = Number(target.value);
+ break;
+
+ default:
+ val = target.value;
+ }
+
+ if (index != null) {
+ const updatedEndUsage = [...this.state.endUsages];
+ updatedEndUsage[index][name] = val;
+ this.setState({ endUsages: updatedEndUsage });
+
+ } else {
+ this.setState({
+ pendingEndUsage: {
+ ...this.state.pendingEndUsage,
+ [name]: val,
+ },
+ });
+ }
+ }
+
+ generateBillDownload = (data) => {
+ if (data == null || data.length === 0) {
+ return null;
+ }
+
+ // headings
+ let csvString = Object.keys(UtilityDisaggregation.disaggregateDataMapping).join(',');
+
+ csvString += '\n';
+ csvString += data.reduce((acc, val) => {
+ let line = '';
+ const daysInBill = getDateDiff(val.bill_from_date, val.bill_to_date);
+
+ Object.values(UtilityDisaggregation.disaggregateDataMapping).map(columnName => {
+ if (columnName === 'usage') {
+ line += `${val.heating + val.cooling + val.other},`;
+ return val.heating + val.cooling + val.other;
+
+ } else if (columnName === 'days_in_bill') {
+ line += `${daysInBill},`;
+ return daysInBill;
+ }
+
+ line += `${val[columnName]},`;
+ return val[columnName];
+ });
+
+ // Remove whitespace created by the template formatting
+ line = line.replace(' ', '');
+ return `${acc}${line}\n`;
+ }, '');
+
+ const mimeType = 'text/csv;encoding:utf-8';
+ const href = URL.createObjectURL(new Blob([csvString], {
+ type: mimeType,
+ }));
+
+ return href;
+ };
+
+ disableBtns = () => {
+ return this.props.disableDisaggregateBtn() ||
+ this.state.savingDisaggregateMeta ||
+ this.calculateMiscEndUsage().overLimit;
+ }
renderDownloadBtn = () => {
const { disaggregateData } = this.props;
@@ -24,8 +299,8 @@ class UtilityDisaggregation extends Component {
{ this.props.handleBillGAEvent('Download Disaggregated Bill'); }}
>
@@ -38,7 +313,9 @@ class UtilityDisaggregation extends Component {
color: (this.props.rSquared < LOW_RSQUARED) ? 'red' : 'black',
}}
>
- {this.props.rSquared ? this.props.rSquared.toFixed(3).replace(/^0+/, '') : ''}
+ {this.props.rSquared && this.props.rSquared.toFixed(3).replace(/^0+/, '')}
+ {' '}
+
);
@@ -47,19 +324,101 @@ class UtilityDisaggregation extends Component {
render() {
if (!this.props.accountCreated) {
return ;
+ } else if (this.state.fetchError != null) {
+ return (
+
+ );
+ } else if (this.state.saveError != null) {
+ return (
+
+ );
+ } else if (this.state.loadingDisaggregateMeta) {
+ return ;
}
return (
-
- {this.renderDownloadBtn()}
-
+
Non-weather related usage
+
+
+
+ | End Usage |
+ Percentage |
+
+
+ |
+
+
+
+ {this.state.endUsages.map((disaggregationForm, index) => {
+ return this.state.editMode ?
+ this.deleteEndUsage(index)}
+ form={disaggregationForm}
+ key={index}
+ handleInputChange={(form) => this.handleInputChange(form, index)}
+ showDelete
+ /> :
+
+ | {UtilityDisaggregation.usageTypes[disaggregationForm.usage_type_id]} |
+ {disaggregationForm.value} % |
+ |
+
;
+ })}
+
+ {this.state.editMode && Object.keys(this.state.availableUsageTypes).length > 0 &&
+ this.handleInputChange(event)}
+ />
+ }
+
+
+ | Miscellaneous |
+ {/* eslint-disable no-use-before-define */}
+
+ {this.calculateMiscEndUsage().total} %
+ |
+ |
+
+
+
+
+
+
+ Saving
+
+
+ Disaggregating
+
+
+
+
+
+ {this.renderDownloadBtn()}
{this.props.renderUploadButton('Disaggregated Bill')}
@@ -67,10 +426,28 @@ class UtilityDisaggregation extends Component {
}
}
+const styles = {
+ redText: {
+ color: 'red',
+ textAlign: 'right',
+ },
+ alignRight: {
+ textAlign: 'right',
+ },
+};
+
UtilityDisaggregation.propTypes = {
street_address: PropTypes.string,
- disaggregateData: PropTypes.array, // eslint-disable-line
- form: PropTypes.object, // eslint-disable-line
+ disaggregateData: PropTypes.arrayOf(
+ PropTypes.shape({
+ bill_from_date: PropTypes.string,
+ bill_to_date: PropTypes.string,
+ heating: PropTypes.number,
+ cooling: PropTypes.number,
+ other: PropTypes.number,
+ })
+ ),
+ utility_type: PropTypes.string,
handleBillGAEvent: PropTypes.func,
rSquared: PropTypes.number,
renderUploadButton: PropTypes.func,
@@ -78,6 +455,7 @@ UtilityDisaggregation.propTypes = {
disableDisaggregateBtn: PropTypes.func,
handleDisaggregateUtilityBill: PropTypes.func,
loadingDisaggregate: PropTypes.bool,
+ accountId: PropTypes.number,
};
export default UtilityDisaggregation;
diff --git a/src/components/UtilityLine/UtilityLine.js b/src/components/UtilityLine/UtilityLine.js
index 6336a8cc4d73ac3c2f628455a9ba8d8f6b439899..01b3fa9b610debe069d0f5c82d680decc0c93b30 100644
--- a/src/components/UtilityLine/UtilityLine.js
+++ b/src/components/UtilityLine/UtilityLine.js
@@ -120,8 +120,7 @@ class UtilityLine extends Component {
}
- handleFetchingUtilityBill = (event) => {
- event.preventDefault();
+ handleFetchingUtilityBill = () => {
this.updateLoadingFetchState(true);
this.props.fetchUtilityBill(
this.state.form,
@@ -133,9 +132,8 @@ class UtilityLine extends Component {
);
}
- handleDisaggregateUtilityBill = (event) => {
- event.preventDefault();
- this.updateLoadingDisaggregateState(true);
+ handleDisaggregateUtilityBill = () => {
+
this.props.disaggregateUtilityBill(
this.state.form,
this.state.account_id,
@@ -727,9 +725,9 @@ class UtilityLine extends Component {
);
let scrapeDaysString = '';
if (scrapeDaysElapsed > 0) {
- scrapeDaysString = `Last scraped ${scrapeDaysElapsed} day${scrapeDaysElapsed > 1 ? 's' : ''} ago`;
+ scrapeDaysString = `Last fetched ${scrapeDaysElapsed} day${scrapeDaysElapsed > 1 ? 's' : ''} ago`;
} else {
- scrapeDaysString = 'Last scraped today';
+ scrapeDaysString = 'Last fetched today';
}
const color = scrapeDaysElapsed >= 30 ? 'red' : null;
scrapeDays = (
@@ -811,20 +809,19 @@ class UtilityLine extends Component {
{this.state.accountCreated && 'Disaggregation'}
-
-
-
+
{this.state.accountCreated ? 'View Data' : ''}
diff --git a/src/components/UtilityLine/__snapshots__/DisaggregationForm.test.js.snap b/src/components/UtilityLine/__snapshots__/DisaggregationForm.test.js.snap
new file mode 100644
index 0000000000000000000000000000000000000000..4245836d1f67567b033dfc60bbb43c7302e39895
--- /dev/null
+++ b/src/components/UtilityLine/__snapshots__/DisaggregationForm.test.js.snap
@@ -0,0 +1,53 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DisaggregationForm snapshot with form 1`] = `
+
+`;
diff --git a/src/components/UtilityLine/dataInteraction.js b/src/components/UtilityLine/dataInteraction.js
index e73728e36a7450f78ca5dd7ce4af493deca62c4e..10bba30c241db21889f19f8ce53d98d8899fece6 100644
--- a/src/components/UtilityLine/dataInteraction.js
+++ b/src/components/UtilityLine/dataInteraction.js
@@ -4,7 +4,7 @@ import React from 'react';
*
*/
-const getDateDiff = (date1String, date2String) => {
+export const getDateDiff = (date1String, date2String) => {
const d1 = new Date(date1String);
const d2 = new Date(date2String);
const timeDiff = Math.abs(d2.getTime() - d1.getTime());
@@ -33,13 +33,6 @@ export const generateBillDownload = (data) => {
'ESCO Charge,' +
'Total Charge,' +
'';
- } else {
- // Disaggregated Bill
- csvString += '' +
- 'Heating Usage,' +
- 'Cooling Usage,' +
- 'Other Usage,' +
- '';
}
csvString += '\n';
@@ -57,16 +50,8 @@ export const generateBillDownload = (data) => {
line += `${val.supply_charge},`;
line += `${val.esco_charge},`;
line += `${val.total_charge_bill},`;
- } else {
- const totalUsage = (
- val.heating + val.cooling + val.other
- );
- // Disaggregated Bill
- line += `${totalUsage},`;
- line += `${val.heating},`;
- line += `${val.cooling},`;
- line += `${val.other},`;
}
+
// Remove whitespace created by the template formatting
line = line.replace(' ', '');
return `${acc}${line}\n`;
diff --git a/src/components/UtilityLine/styles.css b/src/components/UtilityLine/styles.css
index 40c1fc1fbfb362ab4d2550d33b8fbb02a0799c64..ed697fb5183ff8b58bce8bb384cc022dcac9e851 100644
--- a/src/components/UtilityLine/styles.css
+++ b/src/components/UtilityLine/styles.css
@@ -1,3 +1,12 @@
.add {
margin-bottom: 0 !important;
}
+
+input[type=number]::-webkit-inner-spin-button,
+input[type=number]::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+}
+
+input[type=number] {
+ -moz-appearance: textfield;
+}
diff --git a/src/utils/restServices.js b/src/utils/restServices.js
index 3c913b672d1c0bed8d3ca5b8293991ad0090611d..a292d8a7ca71ae2f26bc5dfab58cac83d799d9dd 100644
--- a/src/utils/restServices.js
+++ b/src/utils/restServices.js
@@ -31,6 +31,7 @@ export const boxAccessTokenURL = `${documentService}/boxusertoken/`;
export const accountURL = `${utilityService}/account/`;
export const scrapeURL = `${utilityService}/scrape/`;
export const disaggregateURL = `${utilityService}/disaggregate/`;
+export const disaggregateMetaURL = `${utilityService}/disaggregatemeta/`;
export const projectURL = `${projectService}/project/`;
export const contactsURL = `${projectService}/contact/`;