diff --git a/package.json b/package.json index f0a5c583f610a5ec5235712e1a343329ad81cbc2..f4fc62d03376f21e44cad58a260620e6342c8e9f 100644 --- a/package.json +++ b/package.json @@ -55,11 +55,13 @@ }, "dependencies": { "bpl": "git+https://7f8bbb4b0a383ad905fee5b0f7cbc0c22533b556:x-oauth-basic@github.com/Blocp/bpl.git", + "dom-to-image": "^2.5.2", "react": "^15.3.2", "react-dom": "^15.3.2", "react-redux": "^4.4.5", "react-router": "^3.0.0", "react-router-redux": "^4.0.7", + "recharts": "^0.21.2", "redux": "^3.6.0", "redux-saga": "^0.14.2", "whatwg-fetch": "^1.0.0" diff --git a/src/components/TurkHit/index.js b/src/components/TurkHit/index.js index 7ce0d776c55d4f9647870b9dc636beb5582f83ff..09416740f824e51b47485205372d7e90811cd556 100644 --- a/src/components/TurkHit/index.js +++ b/src/components/TurkHit/index.js @@ -58,17 +58,15 @@ class TurkHit extends Component { } if (document) { return ( -
- - Download - -
+ + Download + ); } - return (
); + return (); } renderCreateHitButton = () => { @@ -115,10 +113,10 @@ class TurkHit extends Component {
- - - - + + + +
Status {currStatus.message}
Message {val.response_message}
Creation Date {val.hit_date}
Creator {val.requester_name}
Status{currStatus.message}
Message{val.response_message}
Creation Date{val.hit_date}
Creator{val.requester_name}

diff --git a/src/components/Utilities/index.js b/src/components/Utilities/index.js index dbd1397c298e799d1e12ba0ea1eb0e6ebcfd8b35..2fe2a8c7279aefa6d5d932acc0cfc5b58897d8aa 100644 --- a/src/components/Utilities/index.js +++ b/src/components/Utilities/index.js @@ -1,8 +1,9 @@ -import React, { Component, PropTypes } from 'react'; +import React, { Component, PropTypes, cloneElement } from 'react'; import buildingDetailPropTypes from '../../containers/Building/propTypes'; import request from '../../utils/request'; import { getHeaders, billsURL, accountURL } from '../../utils/restServices'; +import documentsPropType from '../../containers/Documents/propTypes'; import UtilityAccount from '../UtilityAccount'; @@ -22,61 +23,86 @@ class Utilities extends Component { this.getUtilityAccounts(address); } + componentWillReceiveProps(nextProps) { + // Pass down the document URLs to the children once doc service is done + const nextDocuments = nextProps.documents.files.utilityBills; + const curDocuments = this.props.documents.files.utilityBills; + if (nextDocuments !== curDocuments) { + const newAccounts = this.state.accountLines.map((account) => { + /* eslint-disable no-param-reassign */ + const documentURLs = nextDocuments.reduce((acc, val) => { + if (val.key === account.props.documentKeys.scrape) { + acc.scrape = val.url_download; + } else if (val.key === account.props.documentKeys.disaggregate) { + acc.disaggregate = val.url_download; + } + return acc; + }, {}); + return cloneElement(account, { + documentURLs, + }); + }); + this.setState({ accountLines: newAccounts }); + } + } + getUtilityAccounts = (address) => { // First reqeust gets the utility account information request(`${accountURL}${this.props.buildingId}`, { method: 'GET', headers: getHeaders(), - }).then((accountRes) => { - // Second request gets the box files. Need to link them - request(`${billsURL}${this.props.buildingId}?address=${address}`, { - method: 'GET', - headers: getHeaders(), - }).then((billsRes) => { - if ((!accountRes.err && accountRes.utilities) - && (!billsRes.err)) { - const boxBuildingList = billsRes.box_building_list; - const allAccounts = accountRes.utilities.map((account) => { - const urlDownload = boxBuildingList.reduce((url, boxFile) => { - if (!url) { - if (boxFile.tags === account.account_number) { - return boxFile.url_download; - } - } - return url; - }, ''); - - return this.createUtilityComponent({ - building_address: address, - utility: account.type, - account_number: account.account_number, - username: account.login, - password: account.pass, - access_code: account.access_code, - }, - urlDownload, - ); - }); - const blankAccount = this.createUtilityComponent(); - this.setState({ - accountLines: this.state.accountLines.concat(allAccounts, blankAccount), - }); - this.resetErrorMessage(); - } else if (accountRes.err) { - this.setState({ error: `Failed to retrieve utility accounts. | ${accountRes.err.message}` }); - } else if (billsRes.err) { - this.setState({ error: `Failed to retrieve utility bills. | ${billsRes.err.message}` }); - } - }); + }).then((res) => { + const data = res.data; + if (!res.err && data.utilities) { + const allDocumentKeys = data.utilities.reduce((acc, val) => { + acc.push(val.scrape_document_key); + acc.push(val.disaggregate_document_key); + return acc; + }, []); + // Fetch the documents for all of the accounts for this building + this.props.getDocuments([], allDocumentKeys, 'utilityBills'); + const allAccounts = data.utilities.map((account) => { + const documentKeys = { + scrape: account.scrape_document_key, + disaggregate: account.disaggregate_document_key, + }; + return this.createUtilityComponent({ + building_address: address, + utility: account.type, + account_number: account.account_number, + username: account.login, + password: account.pass, + access_code: account.access_code, + }, + account.id, + documentKeys, + this.parseDisaggregateData(account.disaggregated_csv_output), + ); + }); + const blankAccount = this.createUtilityComponent(); + this.setState({ + accountLines: this.state.accountLines.concat(allAccounts, blankAccount), + }); + this.resetErrorMessage(); + } else if (res.err) { + this.setState({ error: `Failed to retrieve utility accounts. | ${res.err.message}` }); + } }); } - updateUtilityBill = (form, updateLoadingState, setDownloadURL) => { + updateUtilityBill = ( + form, + accountId, + updateLoadingFetchState, + setDisaggregateData, + setDocumentURLs + ) => { request(billsURL, { method: 'POST', headers: getHeaders(), body: JSON.stringify({ ...form, + account_id: accountId, building_id: this.props.buildingId, building_address: this.props.building.address, }), @@ -84,13 +110,62 @@ class Utilities extends Component { if (res.err) { this.setState({ error: `Failed to retrieve utility bill. | ${res.err.message}` }); } else { - setDownloadURL(res.download_url); + const data = res.data; + const documentURLs = { disaggregate: data.disaggregate_download_url, + scrape: data.scrape_download_url }; + setDocumentURLs(documentURLs); + const disaggregateData = this.parseDisaggregateData(data.disaggregate_csv_output); + setDisaggregateData(disaggregateData); + this.resetErrorMessage(); } - updateLoadingState(false); + updateLoadingFetchState(false); }); } + updateDisaggregateBill = ( + form, + accountId, + fileContent, + updateLoadingUploadState, + setDisaggregateData) => { + request(`${billsURL}${accountId}`, { + method: 'PUT', + headers: getHeaders(), + body: JSON.stringify({ + ...form, + account_id: accountId, + building_id: this.props.buildingId, + building_address: this.props.building.address, + file_content: fileContent, + }), + }).then((res) => { + if (res.err) { + this.setState({ error: `Failed to update utility bill. | ${res.err.message}` }); + } else { + const data = res.data; + const disaggregateData = this.parseDisaggregateData(data.disaggregate_csv_output); + setDisaggregateData(disaggregateData); + + this.resetErrorMessage(); + } + updateLoadingUploadState(false); + }); + } + + parseDisaggregateData = (data) => { + const disaggregateData = data.map(val => ({ + billFromDate: val.bill_from_date, + billToDate: val.bill_to_date, + daysInBill: val.days_in_bill, + usage: val.usage, + heating: val.heating_usage, + cooling: val.cooling_usage, + other: val.other_usage, + })); + return disaggregateData; + } + createAccount = (form) => { request(accountURL, { method: 'POST', @@ -103,6 +178,10 @@ class Utilities extends Component { }).then((res) => { if (res.err) { this.setState({ error: `There was an error while creating the account. | ${res.err.message}` }); + const allAccounts = this.state.accountLines.slice(); + allAccounts.pop(); + allAccounts.push(this.createUtilityComponent()); + this.setState({ accountLines: allAccounts }); } else { /* NOTE: The reason to remove the last item and re-add it is to update the props of that component. If the user decides to delete the newly @@ -110,7 +189,7 @@ class Utilities extends Component { */ const allAccounts = this.state.accountLines.slice(); allAccounts.pop(); - allAccounts.push(this.createUtilityComponent(form)); + allAccounts.push(this.createUtilityComponent(form, res.data.id)); allAccounts.push(this.createUtilityComponent()); this.setState({ accountLines: allAccounts }); this.resetErrorMessage(); @@ -121,8 +200,9 @@ class Utilities extends Component { deleteAccountLine = (accountLineToRemove) => { const accounts = this.state.accountLines; const accountNumberToRemove = accountLineToRemove.state.form.account_number; + const accountIdToRemove = accountLineToRemove.state.account_id; - request(`${accountURL}${this.props.buildingId}?account_number=${accountNumberToRemove}`, { + request(`${accountURL}${this.props.buildingId}?account_number=${accountNumberToRemove}&account_id=${accountIdToRemove}`, { method: 'DELETE', headers: getHeaders(), }).then((res) => { @@ -139,14 +219,20 @@ class Utilities extends Component { }); } - createUtilityComponent = (incForm, downloadURL = '') => { + createUtilityComponent = ( + incForm, + accountId = null, + documentKeys = {}, + disaggregateData = []) => { let accountProps = {}; const { accountsCounter } = this.state; - if (incForm !== undefined) { accountProps = { form: incForm, - downloadURL, + account_id: accountId, + disaggregateData, + documentKeys, + documentURLs: {}, disabled: true, }; } @@ -162,6 +248,7 @@ class Utilities extends Component { createAccount={this.createAccount} deleteAccount={this.deleteAccountLine} updateUtilityBill={this.updateUtilityBill} + updateDisaggregateBill={this.updateDisaggregateBill} /> ); } @@ -188,6 +275,8 @@ class Utilities extends Component { Utilities.propTypes = { buildingId: PropTypes.string, building: buildingDetailPropTypes, + getDocuments: PropTypes.func, + documents: documentsPropType, }; export default Utilities; diff --git a/src/components/UtilityAccount/index.js b/src/components/UtilityAccount/index.js index 9d052709828bb1fa941acf554168418226c70837..e8003ccadb3039ab44eacb45df490d3ae1f159a7 100644 --- a/src/components/UtilityAccount/index.js +++ b/src/components/UtilityAccount/index.js @@ -1,4 +1,7 @@ import React, { Component, PropTypes } from 'react'; +import { LineChart, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Line } from 'recharts'; +import domtoimage from 'dom-to-image'; +import { uploadSVG } from '../bpl'; import './styles.css'; class UtilityAccount extends Component { @@ -13,17 +16,43 @@ class UtilityAccount extends Component { password: this.props.form.password, access_code: this.props.form.access_code, }, - downloadURL: this.props.downloadURL, + account_id: this.props.account_id, + disaggregateData: this.props.disaggregateData, + documentKeys: { + scrape: this.props.documentKeys.scrape, + disaggregate: this.props.documentKeys.disaggregate, + }, + documentURLs: { + scrape: this.props.documentURLs.scrape, + disaggregate: this.props.documentURLs.disaggregate, + }, disabled: this.props.disabled, - loading: false, + convertingFile: false, + displayChart: false, + loadingFetch: false, + loadingUpload: false, }; } - setDownloadURL = url => ( - this.setState({ downloadURL: url }) + + componentWillReceiveProps(nextProps) { + // Update the document URLs + this.setState({ documentURLs: nextProps.documentURLs }); + } + + setDocumentURLs = urls => ( + this.setState({ documentURLs: urls }) + ) + + setDisaggregateData = data => ( + this.setState({ disaggregateData: data }) + ) + + updateLoadingFetchState = isLoading => ( + this.setState({ loadingFetch: isLoading }) ) - updateLoadingState = isLoading => ( - this.setState({ loading: isLoading }) + updateLoadingUploadState = isLoading => ( + this.setState({ loadingUpload: isLoading }) ) handleInputChange = event => ( @@ -34,11 +63,31 @@ class UtilityAccount extends Component { }, }) ) + downloadSVG = (event) => { + event.preventDefault(); + const chartWrapper = document.getElementsByClassName(this.state.form.account_number)[0]; + domtoimage.toPng(chartWrapper, { bgcolor: '#fff' }) + .then((dataUrl) => { + // Create an anchor and force a click to download + const anchor = document.createElement('a'); + anchor.style.display = 'none'; + anchor.setAttribute('href', dataUrl); + const fileName = `disaggregate_chart_${this.state.form.account_number}.png`; + anchor.setAttribute('download', fileName); + anchor.click(); + anchor.remove(); + }); + } handleUpdatingUtilityBill = (event) => { event.preventDefault(); - this.updateLoadingState(true); - this.props.updateUtilityBill(this.state.form, this.updateLoadingState, this.setDownloadURL); + this.updateLoadingFetchState(true); + this.props.updateUtilityBill( + this.state.form, + this.state.account_id, + this.updateLoadingFetchState, + this.setDisaggregateData, + this.setDocumentURLs); } handleCreateAccount = (event) => { @@ -52,11 +101,39 @@ class UtilityAccount extends Component { this.props.deleteAccount(this); } + toggleChartDisplay = (event) => { + event.preventDefault(); + this.setState({ displayChart: !this.state.displayChart }); + } + isAccountNumberLong = () => this.state.form.account_number.length > 15; showAccountWarning = () => (this.isAccountNumberLong() ?

Account number is too long
:
); + + uploadHandler = (event) => { + const file = event.target.files[0]; + if (file) { + this.setState({ convertingFile: true }); + const fileReader = new FileReader(); + + fileReader.onload = function fileResults() { + const base64result = fileReader.result.split(',')[1]; + this.updateLoadingUploadState(true); + this.setState({ convertingFile: false }); + this.props.updateDisaggregateBill( + this.state.form, + this.state.account_id, + base64result, + this.updateLoadingUploadState, + this.setDisaggregateData, + ); + }.bind(this); + fileReader.readAsDataURL(file); + } + } + renderAddAccountBtn = () => { let isDisabled = !(this.state.form.account_number); if (this.state.form.utility === 'national_grid_gas') { @@ -75,28 +152,101 @@ class UtilityAccount extends Component { ); } + renderUploadButton = () => { + let disabled = false; + let img = ( +
+ +
Upload
Disaggregation +
+ ); + if (this.state.convertingFile || this.state.loadingUpload) { + disabled = true; + img = ( +
+
+
+
+
+
+ ); + } + return ( +
+ + +
+ ); + } + + renderFetchAndDownloadBtn = () => { - const { downloadURL } = this.state; - let visibility = 'hidden'; - if (downloadURL !== undefined && downloadURL !== '') { - visibility = 'visible'; + const { scrape, disaggregate } = this.state.documentURLs; + let scrapeVisibility = 'hidden'; + if (scrape !== undefined && scrape !== '') { + scrapeVisibility = 'visible'; + } + let disaggregateVisibility = 'hidden'; + if (disaggregate !== undefined && disaggregate !== '') { + disaggregateVisibility = 'visible'; + } + let chartToggle = (); + let chartDownload = (); + if (this.state.disaggregateData.length > 0) { + chartToggle = ( + + ); + if (this.state.displayChart) { + chartDownload = ( + + ); + } } return (
- Download + Scraped Bill + + Disaggregated Bill + + {chartToggle} + {chartDownload} + {this.renderUploadButton()}