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

+ + + + + + + + + + {this.state.endUsages.map((disaggregationForm, index) => { + return this.state.editMode ? + this.deleteEndUsage(index)} + form={disaggregationForm} + key={index} + handleInputChange={(form) => this.handleInputChange(form, index)} + showDelete + /> : + + + + ; + })} + + {this.state.editMode && Object.keys(this.state.availableUsageTypes).length > 0 && + this.handleInputChange(event)} + /> + } + + + + {/* eslint-disable no-use-before-define */} + + + +
End UsagePercentage + +
{UtilityDisaggregation.usageTypes[disaggregationForm.usage_type_id]}{disaggregationForm.value} % +
Miscellaneous + {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/`;