diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..c67846a60bc57af0480de220d709e565245cd9e7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "editor.tabSize": 2 +} \ No newline at end of file diff --git a/src/components/Utilities/index.js b/src/components/Utilities/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e6264e180e44a82471906253bf6cb6e9ba6f4b3c --- /dev/null +++ b/src/components/Utilities/index.js @@ -0,0 +1,165 @@ +import React, { Component, PropTypes } from 'react'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; +import buildingDetailPropTypes from '../../containers/BuildingOverview/buildingDetailPropType'; + +import request from '../../utils/request'; +import { getHeaders, utilityURL } from '../../utils/rest_services'; + +import { loadBuildingDetail } from '../../containers/BuildingOverview/actions'; +import UtilityForm from '../UtilityForm'; + +class Utilities extends Component { + constructor(props) { + super(props); + + this.state = { + accountLines: [], + error: false, + }; + } + + componentDidMount() { + const { address, building_id } = this.props.buildingDetail.overview; + + // TODO remove getting adress, should be done in backend + /* eslint-disable camelcase */ + if (!address || this.props.buildingId !== building_id) { + this.props.loadBuildingDetail(this.props.buildingId); + } else if (address && this.props.buildingId === building_id) { + this.getUtilityAccounts(address); + } + } + + componentDidUpdate(prevProps) { + const { address, building_id } = this.props.buildingDetail.overview; + const prevAddress = prevProps.buildingDetail.overview.address; + if (parseInt(this.props.buildingId, 10) === building_id && prevAddress !== address) { + this.getUtilityAccounts(address); + } + } + + getUtilityAccounts = (address) => { + request(`${utilityURL}${this.props.buildingId}?address=${address}`, { + method: 'GET', + headers: getHeaders(), + }).then((res) => { + if (!res.err && res.data.utilities) { + res.data.utilities.forEach((item) => { + this.addAccountLine({ + building_address: address, + utility: item.type, + account_number: item.account_number, + username: item.login, + password: item.pass, + access_code: item.access_code, + }, + item.url_download, + true); + }); + this.addAccountLine(); + } else if (res.err) { + this.setState({ error: res.err.message }); + } + }); + } + + getUtilityBill = (form) => { + request(utilityURL, { + method: 'PUT', + headers: getHeaders(), + body: JSON.stringify({ + ...form, + building_id: parseInt(this.props.buildingId, 10), + building_address: this.props.buildingDetail.overview.address, + }), + }).then((res) => { + if (res.err) { + this.setState({ error: 'Failed to retrieve utility bill' }); + } + }); + } + + createAccount = (form) => { + request(utilityURL, { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify({ + ...form, + building_id: parseInt(this.props.buildingId, 10), + building_address: this.props.buildingDetail.overview.address, + }), + }).then((res) => { + if (res.err) { + this.setState({ error: 'Failed to create account' }); + } + }); + this.addAccountLine(); + } + + deleteAccountLine = (lineItem) => { + const accounts = this.state.accountLines; + const accountToRemove = lineItem.props.form.account_number; + + request(`${utilityURL}${this.props.buildingId}?account_number=${accountToRemove}`, { + method: 'DELETE', + headers: getHeaders(), + }).then((res) => { + if (!res.err) { + /* eslint-disable arrow-body-style */ + // TODO remove eslint disable + const newAccounts = accounts.filter((item) => { + return item.props.form.account_number !== accountToRemove; + }); + /* eslint-enable */ + this.setState({ accountLines: newAccounts }); + } else { + this.setState({ error: 'Failed to delete account' }); + } + }); + } + + addAccountLine = (form, downloadURL, disabled = false) => { + const curr = this.state.accountLines; + curr.push(); + this.setState({ accountLines: curr }); + } + + render() { + return ( +
+
+ {this.state.error} +
+ {this.state.accountLines} +
+ ); + } +} + +Utilities.propTypes = { + buildingId: PropTypes.string, + buildingDetail: buildingDetailPropTypes, + loadBuildingDetail: PropTypes.func, +}; + +function mapDispatchToProps(dispatch) { + return bindActionCreators({ loadBuildingDetail }, dispatch); +} + +function mapStateToProps({ buildingDetail }) { + return { buildingDetail }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(Utilities); diff --git a/src/components/Utilities/styles.css b/src/components/Utilities/styles.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/components/UtilityForm/index.js b/src/components/UtilityForm/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ee6c64b07148bad09f9e3e8cb354e74d8cc7733e --- /dev/null +++ b/src/components/UtilityForm/index.js @@ -0,0 +1,153 @@ +import React, { Component, PropTypes } from 'react'; +import './styles.css'; + +class UtilityForm extends Component { + constructor(props) { + super(props); + + this.state = { + disabled: this.props.disabled, + form: { + utility: this.props.form.utility, + account_number: this.props.form.account_number, + username: this.props.form.username, + password: this.props.form.password, + access_code: this.props.form.access_code, + }, + downloadURL: this.props.downloadURL, + }; + } + + getUtilityBill = (event) => { + event.preventDefault(); + this.props.getUtilityBill(this.state.form); + } + + handleInputChange = (event) => { + this.setState({ + form: { + ...this.state.form, + [event.target.name]: event.target.value, + }, + }); + } + + handleCreateAccount = (event) => { + event.preventDefault(); + this.setState({ disabled: true }); + this.props.createAccount(this.state.form); + } + + handleDeleteAccount = (event) => { + event.preventDefault(); + this.props.deleteAccount(this); + } + + render() { + const natGrid = this.state.form.utility === 'national_grid_gas' ? '' : 'hidden'; + return ( +
+
+ + +
+ + +
+ {!this.state.disabled || +
+ + Download + + + +
+ } + {this.state.disabled || + + } +
+
+
+ ); + } +} + +UtilityForm.propTypes = { + disabled: PropTypes.bool, + form: PropTypes.shape({ + building_address: PropTypes.string, + utility: PropTypes.string, + account_number: PropTypes.string, + access_code: PropTypes.string, + username: PropTypes.string, + password: PropTypes.string, + }), + downloadURL: PropTypes.string, + createAccount: PropTypes.func, + deleteAccount: PropTypes.func, + getUtilityBill: PropTypes.func, +}; + +UtilityForm.defaultProps = { + disable: false, + form: { + building_address: '', + utility: 'con_edison_electric', + account_number: '', + access_code: '', + username: '', + password: '', + }, +}; + +export default UtilityForm; diff --git a/src/components/UtilityForm/styles.css b/src/components/UtilityForm/styles.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/containers/BuildingOverview/buildingDetailPropType.js b/src/containers/BuildingOverview/buildingDetailPropType.js new file mode 100644 index 0000000000000000000000000000000000000000..6cdf609e2a65e0332b25b5d8886f9ee109565b77 --- /dev/null +++ b/src/containers/BuildingOverview/buildingDetailPropType.js @@ -0,0 +1,20 @@ +import { PropTypes } from 'react'; + +export default PropTypes.shape({ + overview: PropTypes.shape({ + address: PropTypes.string, + bbl: PropTypes.number, + building_id: PropTypes.number, + lot_id: PropTypes.number, + borough: PropTypes.string, + zipcode: PropTypes.number, + loading: PropTypes.boolean, + error: PropTypes.boolean, + }), + hit: PropTypes.shape({ + id: PropTypes.string, + status: PropTypes.string, + loading: PropTypes.boolean, + error: PropTypes.boolean, + }), +}); diff --git a/src/containers/BuildingOverview/index.js b/src/containers/BuildingOverview/index.js index c813797f51b75ef17911e9852cbb624cdcd59ce9..24e62a013bad97834df47bcb6eea584a9196f5fd 100644 --- a/src/containers/BuildingOverview/index.js +++ b/src/containers/BuildingOverview/index.js @@ -1,6 +1,7 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; +import buildingDetailPropTypes from './buildingDetailPropType'; import { loadBuildingDetail, @@ -58,24 +59,7 @@ class BuildingOverview extends Component { } BuildingOverview.propTypes = { - buildingDetail: PropTypes.shape({ - overview: PropTypes.shape({ - address: PropTypes.string, - bbl: PropTypes.number, - building_id: PropTypes.number, - lot_id: PropTypes.number, - borough: PropTypes.string, - zipcode: PropTypes.number, - loading: PropTypes.boolean, - error: PropTypes.boolean, - }), - hit: PropTypes.shape({ - id: PropTypes.string, - status: PropTypes.string, - loading: PropTypes.boolean, - error: PropTypes.boolean, - }), - }), + buildingDetail: buildingDetailPropTypes, params: PropTypes.objectOf(PropTypes.string), loadBuildingDetail: PropTypes.func, loadHit: PropTypes.func, diff --git a/src/routes.js b/src/routes.js index ef2a881642aef3532cc2eafddc14f6d29ceb20d6..c64dc2a6222300e7819750bba89ad3e9e19d5082 100644 --- a/src/routes.js +++ b/src/routes.js @@ -9,6 +9,7 @@ import HomePage from './screens/HomePage'; import BuildingDetail from './screens/BuildingDetail'; import BuildingOverview from './containers/BuildingOverview'; +import Utilities from './components/Utilities'; import Dummy from './components/dummyComponent'; export default ( @@ -23,7 +24,7 @@ export default ( - + diff --git a/src/screens/BuildingDetail/index.js b/src/screens/BuildingDetail/index.js index 620e993e60e5fec0df6333648b3b34c8be76f47d..85cfdc0daef6938caade0bc24aa1dc0eb9a67516 100644 --- a/src/screens/BuildingDetail/index.js +++ b/src/screens/BuildingDetail/index.js @@ -12,7 +12,9 @@ export default function BuildingDetail(props) {
- {props.children} + {props.children && React.cloneElement(props.children, { + buildingId: props.params.buildingID, + })}
); diff --git a/src/utils/rest_services.js b/src/utils/rest_services.js index 04fb1634d10ae80e1b7d7a3cd7f6aff2212b21bb..4f2a2c167ab086b5a428ecc9896e54d78bcadb97 100644 --- a/src/utils/rest_services.js +++ b/src/utils/rest_services.js @@ -5,5 +5,9 @@ export function getHeaders() { }); } -export const buildingsURL = `${process.env.REACT_APP_BUILDING_SERVICE}/building/`; -export const turkURL = `${process.env.REACT_APP_BUILDING_SERVICE}/turkhit/`; +const buildingService = process.env.REACT_APP_BUILDING_SERVICE; +const utilityService = process.env.REACT_APP_UTILITY_SERVICE; + +export const buildingsURL = `${buildingService}/building/`; +export const turkURL = `${buildingService}/turkhit/`; +export const utilityURL = `${utilityService}/scraper/`; diff --git a/typings.json b/typings.json new file mode 100644 index 0000000000000000000000000000000000000000..520f8bbf1c19eedd4221e0a6d1b7aa7f2492cf5c --- /dev/null +++ b/typings.json @@ -0,0 +1,12 @@ +{ + "name": "buildings", + "dependencies": {}, + "compilerOptions": { + "target": "ES6", + "module": "commonjs", + "allowSyntheticDefaultImports": true + }, + "exclude": [ + "node_modules" + ] +}