diff --git a/src/components/BuildingOverview/index.js b/src/components/BuildingOverview/index.js index 12b083fa311a599b6236a12a178f5ef47ebefe0e..df9a359b83a4829e371ece6eb3436ab967f65144 100644 --- a/src/components/BuildingOverview/index.js +++ b/src/components/BuildingOverview/index.js @@ -1,24 +1,52 @@ -import React, { PropTypes } from 'react'; +import React, { Component, PropTypes } from 'react'; +import documentsPropType from '../../containers/Documents/propTypes'; +import DocumentCardViewer from '../../components/DocumentCardViewer'; import './styles.css'; -export default function BuildingOverview({ building }) { - return ( -
-

- {building.address} -

- -
- ); + +export default class BuildingOverview extends Component { + constructor(props) { + super(props); + + const { building, buildingId } = this.props; + this.state = { + documentPath: `/Buildings/${buildingId}_${building.address}/`, + fileKey: 'building', + }; + } + + render() { + const { building } = this.props; + + return ( +
+
+

+ {building.address} +

+ +
+ +
+ ); + } } BuildingOverview.propTypes = { + buildingId: PropTypes.string, building: PropTypes.shape({ address: PropTypes.string, bbl: PropTypes.number, @@ -27,4 +55,7 @@ BuildingOverview.propTypes = { borough: PropTypes.string, zipcode: PropTypes.number, }), + documents: documentsPropType, + getDocuments: PropTypes.func, + uploadDocument: PropTypes.func, }; diff --git a/src/components/DocumentCard/index.js b/src/components/DocumentCard/index.js new file mode 100644 index 0000000000000000000000000000000000000000..562a35398b98c4a38ffb773247a9a3db82a22d60 --- /dev/null +++ b/src/components/DocumentCard/index.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { documentProps } from '../../containers/Documents/propTypes'; + +const DocumentCard = (props) => { + const { document } = props; + let date = ''; + if (document.updated !== null) { + date = `Last modified on ${new Date(document.updated)}`; + } else { + date = `Created on ${new Date(document.created)}`; + } + + return ( + + {document.name} + { document.tags !== '' + ? {document.tags} + : } + {date} + + ); +}; + +DocumentCard.propTypes = { + document: documentProps, +}; + +export default DocumentCard; diff --git a/src/components/DocumentCard/styles.css b/src/components/DocumentCard/styles.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/components/DocumentCardViewer/index.js b/src/components/DocumentCardViewer/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a17ec11a2ed8c68adf5544207491d83aafa0de18 --- /dev/null +++ b/src/components/DocumentCardViewer/index.js @@ -0,0 +1,113 @@ +import React, { Component, PropTypes } from 'react'; + +import documentsPropType from '../../containers/Documents/propTypes'; +import DocumentCard from '../DocumentCard/'; +import ErrorAlert from '../ErrorAlert'; +import { uploadSVG } from '../bpl'; +import './styles.css'; + +export default class DocumentCardViewer extends Component { + constructor(props) { + super(props); + + this.state = { + convertingFile: false, + }; + } + + componentDidMount() { + this.props.getDocuments(this.props.documentPath, this.props.fileKey); + } + + uploadHandler = (event) => { + const file = event.target.files[0]; + const { buildingId, documentPath, fileKey } = this.props; + + this.setState({ convertingFile: true }); + const fileReader = new FileReader(); + + fileReader.onload = function fileResults() { + this.setState({ convertingFile: false }); + this.props.uploadDocument(buildingId, documentPath, fileReader.result, '', + file.name, fileKey); + }.bind(this); + fileReader.readAsDataURL(file); + } + + renderUploadButton = () => { + let disabled = false; + let img = ( +
+ + Choose a file +
+ ); + if (this.state.convertingFile || this.props.documents.uploading) { + disabled = true; + img = ( +
+
+
+
+
+
+ ); + } + + return ( +
+ + +
+ ); + } + + renderDocuments = () => { + if (this.props.documents.error) { + return
; + } + const { building } = this.props.documents.files; + const docs = building.map(item => ( +
+ +
+ )); + return docs; + } + + render() { + const { error, uploadError } = this.props.documents; + return ( +
+ +
Documents + {/* eslint-disable jsx-a11y/href-no-hash */} + View all +
+ { this.renderUploadButton() } +
{ this.renderDocuments() }
+
+ ); + } +} + +DocumentCardViewer.propTypes = { + buildingId: PropTypes.string.isRequired, + documentPath: PropTypes.string.isRequired, + fileKey: PropTypes.string.isRequired, + documents: documentsPropType.isRequired, + getDocuments: PropTypes.func.isRequired, + uploadDocument: PropTypes.func.isRequired, +}; diff --git a/src/components/DocumentCardViewer/styles.css b/src/components/DocumentCardViewer/styles.css new file mode 100644 index 0000000000000000000000000000000000000000..2552bcb9ce5fcfa0d411742273058dd09e7c4b13 --- /dev/null +++ b/src/components/DocumentCardViewer/styles.css @@ -0,0 +1,13 @@ +.documentCardViewer { + /* TODO: Fix max height, 68px is height of navbar, 27px is the padding */ + max-height: calc(95vh - 68px - 27px); + display: flex; + flex-direction: column; +} + +.scrollBox { + overflow: auto; + flex: 1; + width: 95%; + margin: 0 auto; +} diff --git a/src/components/ErrorAlert/index.js b/src/components/ErrorAlert/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c0862b7e421033cc037f9b0ce2098d9f45a4905c --- /dev/null +++ b/src/components/ErrorAlert/index.js @@ -0,0 +1,31 @@ +import React, { PropTypes } from 'react'; + +const ErrorAlert = (props) => { + const { error, genericMessage } = props; + let errMessage = ''; + + if (error && genericMessage !== undefined) { + errMessage = `${genericMessage} | ${error.message}`; + } else if (error) { + errMessage = error.message; + } + + return ( +
+ {errMessage} +
+ ); +}; + +ErrorAlert.propTypes = { + error: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.instanceOf(Error), + ]).isRequired, + genericMessage: PropTypes.string, +}; + +export default ErrorAlert; diff --git a/src/components/bpl.js b/src/components/bpl.js index a865f314df6b2130deaae8669f7be490b888dad0..1349c49d46ccd1e8884a4a6f7553ada7f2f0ad6c 100644 --- a/src/components/bpl.js +++ b/src/components/bpl.js @@ -1,3 +1,5 @@ +import uploadSVG from 'bpl/dist/svg/upload.svg'; + const BPAnalyticsSVG = require('bpl/dist/svg/BlocPower_Analytics.svg'); const BPAuditSVG = require('bpl/dist/svg/BlocPower_Audit.svg'); const BPBuildingsSVG = require('bpl/dist/svg/BlocPower_Buildings.svg'); @@ -27,6 +29,7 @@ const LogoPNG = require('bpl/dist/img/logo.png'); export { + uploadSVG, BPAnalyticsSVG, BPAuditSVG, BPBuildingsSVG, diff --git a/src/containers/Building/index.js b/src/containers/Building/index.js index 43d300ae15a2263c3b28b4b544853dbcf2d31e10..63b5d823f97eaf75d48ed7145cb5950aa56ba2dd 100644 --- a/src/containers/Building/index.js +++ b/src/containers/Building/index.js @@ -1,15 +1,18 @@ import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import buildingDetailPropTypes from './propTypes'; +import buildingDetailPropTypes from './propTypes'; import { loadBuildingDetail } from './actions'; -import './styles.css'; +import documentsPropType from '../Documents/propTypes'; +import { loadDocuments, uploadDocument } from '../Documents/actions'; + import SideBarDetail from '../../components/SideBarDetail'; +import './styles.css'; -class BuildingOverview extends Component { +class Building extends Component { componentDidMount() { this.props.loadBuildingDetail(this.props.buildingId); } @@ -21,6 +24,9 @@ class BuildingOverview extends Component { mainContent = React.cloneElement(this.props.children, { buildingId: this.props.buildingId, building: this.props.buildingDetail.overview, + documents: this.props.documents, + getDocuments: this.props.loadDocuments, + uploadDocument: this.props.uploadDocument, }); } @@ -43,21 +49,26 @@ class BuildingOverview extends Component { } } -BuildingOverview.propTypes = { +Building.propTypes = { children: PropTypes.element, buildingDetail: buildingDetailPropTypes, buildingId: PropTypes.string, loadBuildingDetail: PropTypes.func, + documents: documentsPropType, + loadDocuments: PropTypes.func, + uploadDocument: PropTypes.func, }; function mapDispatchToProps(dispatch) { return bindActionCreators({ loadBuildingDetail, + loadDocuments, + uploadDocument, }, dispatch); } -function mapStateToProps({ buildingDetail }) { - return { buildingDetail }; +function mapStateToProps({ buildingDetail, documents }) { + return { buildingDetail, documents }; } -export default connect(mapStateToProps, mapDispatchToProps)(BuildingOverview); +export default connect(mapStateToProps, mapDispatchToProps)(Building); diff --git a/src/containers/Documents/actions.js b/src/containers/Documents/actions.js new file mode 100644 index 0000000000000000000000000000000000000000..3c8c901d0a09fe43cd326c81dc8748eb7e913afd --- /dev/null +++ b/src/containers/Documents/actions.js @@ -0,0 +1,59 @@ +import { + LOAD_DOCUMENTS, + LOAD_DOCUMENTS_SUCCESS, + LOAD_DOCUMENTS_ERROR, + UPLOAD_DOCUMENT, + UPLOAD_DOCUMENT_SUCCESS, + UPLOAD_DOCUMENT_ERROR, +} from './constants'; + +export function loadDocuments(documentPath, fileKey) { + return { + type: LOAD_DOCUMENTS, + documentPath, + fileKey, + }; +} + +export function documentsLoaded(documents, fileKey) { + return { + type: LOAD_DOCUMENTS_SUCCESS, + payload: documents, + fileKey, + }; +} + +export function documentsLoadingError(error) { + return { + type: LOAD_DOCUMENTS_ERROR, + error, + }; +} + +export function uploadDocument(buildingId, documentPath, document, tags, + name, fileKey) { + return { + type: UPLOAD_DOCUMENT, + buildingId, + documentPath, + document, + tags, + name, + fileKey, + }; +} + +export function documentUploaded(document, fileKey) { + return { + type: UPLOAD_DOCUMENT_SUCCESS, + document, + fileKey, + }; +} + +export function documentUploadError(error) { + return { + type: UPLOAD_DOCUMENT_ERROR, + error, + }; +} diff --git a/src/containers/Documents/constants.js b/src/containers/Documents/constants.js new file mode 100644 index 0000000000000000000000000000000000000000..ef186ca834f1d74977437bed10a2b46d55f4414a --- /dev/null +++ b/src/containers/Documents/constants.js @@ -0,0 +1,6 @@ +export const LOAD_DOCUMENTS = 'LOAD_DOCUMENTS'; +export const LOAD_DOCUMENTS_SUCCESS = 'LOAD_DOCUMENTS_SUCCESS'; +export const LOAD_DOCUMENTS_ERROR = 'LOAD_DOCUMENTS_ERROR'; +export const UPLOAD_DOCUMENT = 'UPLOAD_DOCUMENT'; +export const UPLOAD_DOCUMENT_SUCCESS = 'UPLOAD_DOCUMENT_SUCCESS'; +export const UPLOAD_DOCUMENT_ERROR = 'UPLOAD_DOCUMENT_ERROR'; diff --git a/src/containers/Documents/propTypes.js b/src/containers/Documents/propTypes.js new file mode 100644 index 0000000000000000000000000000000000000000..f6f413bea6587c5718eee30f77bbecc2f986dfbf --- /dev/null +++ b/src/containers/Documents/propTypes.js @@ -0,0 +1,31 @@ +import { PropTypes } from 'react'; + +const { shape, arrayOf, oneOfType, string, number, bool } = PropTypes; + +export const documentProps = shape({ + box_id: number, + building_id: number, + content_type: string, + created: string, + id: number, + key: string, + name: string, + path: string, + tags: string, + updated: string, + url_box: string, + url_download: string, +}); + +export default shape({ + loading: bool, + error: oneOfType([ + bool, + string, + ]), + files: shape({ + building: arrayOf(documentProps), + utilityBills: arrayOf(documentProps), + buildingDimensions: arrayOf(documentProps), + }), +}); diff --git a/src/containers/Documents/reducer.js b/src/containers/Documents/reducer.js new file mode 100644 index 0000000000000000000000000000000000000000..b33e2bdf6e057d23f41de102bb2b4d619de70d7d --- /dev/null +++ b/src/containers/Documents/reducer.js @@ -0,0 +1,78 @@ +import { + LOAD_DOCUMENTS, + LOAD_DOCUMENTS_SUCCESS, + LOAD_DOCUMENTS_ERROR, + UPLOAD_DOCUMENT, + UPLOAD_DOCUMENT_SUCCESS, + UPLOAD_DOCUMENT_ERROR, +} from './constants'; + +const initState = { + loading: false, + error: false, + uploading: false, + uploadError: false, + files: { + building: [], // Root Directory + utilityBills: [], // Utility_Bills + buildingDimensions: [], // Building_Dimensions + }, +}; + +export default function (state = initState, action) { + switch (action.type) { + case LOAD_DOCUMENTS: + return { + ...state, + loading: true, + error: false, + }; + + case LOAD_DOCUMENTS_SUCCESS: + return { + ...state, + loading: false, + error: false, + files: { + ...state.files, + [action.fileKey]: action.payload.data.data, + }, + }; + + case LOAD_DOCUMENTS_ERROR: + return { + ...state, + loading: false, + error: action.error, + }; + + case UPLOAD_DOCUMENT: + return { + ...state, + uploading: true, + uploadError: false, + }; + + case UPLOAD_DOCUMENT_SUCCESS: + return { + ...state, + uploading: false, + uploadError: false, + files: { + ...state.files, + [action.fileKey]: [action.document.data.data, + ...state.files[action.fileKey]], + }, + }; + + case UPLOAD_DOCUMENT_ERROR: + return { + ...state, + uploading: false, + uploadError: action.error, + }; + + default: + return state; + } +} diff --git a/src/containers/Documents/sagas.js b/src/containers/Documents/sagas.js new file mode 100644 index 0000000000000000000000000000000000000000..ee27b1024b9b261692eb7d38f8f8eff44b85707b --- /dev/null +++ b/src/containers/Documents/sagas.js @@ -0,0 +1,64 @@ +import { call, put, takeLatest } from 'redux-saga/effects'; +import request from '../../utils/request'; +import { getHeaders, documentURL } from '../../utils/rest_services'; + +import { + LOAD_DOCUMENTS, + UPLOAD_DOCUMENT, +} from './constants'; + +import { + documentsLoaded, + documentsLoadingError, + documentUploaded, + documentUploadError, +} from './actions'; + + +function* getDocuments(action) { + const { documentPath, fileKey } = action; + + const res = yield call( + request, + `${documentURL}?paths[]=${documentPath}`, { + method: 'GET', + headers: getHeaders(), + } + ); + + if (!res.err) { + yield put(documentsLoaded(res, fileKey)); + } else { + yield put(documentsLoadingError(res.err)); + } +} + +function* uploadDocument(action) { + const { buildingId, documentPath, document, tags, name, fileKey } = action; + + const res = yield call( + request, + `${documentURL}`, { + method: 'POST', + headers: getHeaders(), + body: JSON.stringify({ + building_id: buildingId, + path: documentPath, + data: document, + name, + tags, + }), + } + ); + + if (!res.err) { + yield put(documentUploaded(res, fileKey)); + } else { + yield put(documentUploadError(res.err)); + } +} + +export default function* () { + yield takeLatest(LOAD_DOCUMENTS, getDocuments); + yield takeLatest(UPLOAD_DOCUMENT, uploadDocument); +} diff --git a/src/reducers.js b/src/reducers.js index bf22b798707bf26ff0fe8a971879d17a5c4eb729..93772d98886039e84fefde37025c406163f009af 100644 --- a/src/reducers.js +++ b/src/reducers.js @@ -5,6 +5,7 @@ import SearchBarReducer from './containers/SearchBar/reducer'; import BuildingReducer from './containers/Building/reducer'; import GoogleLoginReducer from './containers/GoogleLogin/reducer'; import DimensionsReducer from './containers/Dimensions/reducer'; +import documents from './containers/Documents/reducer'; export default combineReducers({ routing: routerReducer, @@ -12,4 +13,5 @@ export default combineReducers({ buildingDetail: BuildingReducer, googleLogin: GoogleLoginReducer, dimensions: DimensionsReducer, + documents, }); diff --git a/src/sagas.js b/src/sagas.js index bd7d214ac1d3888e08e6c226689228c9b2af0e66..79cf9f7399e11c0abf645af3a5aecb49c8228c8e 100644 --- a/src/sagas.js +++ b/src/sagas.js @@ -1,11 +1,13 @@ import buildingsSearchSaga from './containers/SearchBar/sagas'; import buildingSaga from './containers/Building/sagas'; import dimensionsSaga from './containers/Dimensions/sagas'; +import documentsSaga from './containers/Documents/sagas'; export default function* rootSaga() { yield [ buildingSaga(), dimensionsSaga(), buildingsSearchSaga(), + documentsSaga(), ]; } diff --git a/src/utils/rest_services.js b/src/utils/rest_services.js index a335307818bd6c133f9ca14086987c0a66235a8f..bfcf7f6be0f0e958dd0960d5de65589f54e0ad0a 100644 --- a/src/utils/rest_services.js +++ b/src/utils/rest_services.js @@ -7,8 +7,10 @@ export function getHeaders() { const buildingService = process.env.REACT_APP_BUILDING_SERVICE; const utilityService = process.env.REACT_APP_UTILITY_SERVICE; +const documentService = process.env.REACT_APP_DOCUMENT_SERVICE; export const buildingsURL = `${buildingService}/building/`; export const turkURL = `${buildingService}/turkhit/`; +export const documentURL = `${documentService}/document/`; export const accountURL = `${utilityService}/account/`; export const billsURL = `${utilityService}/bills/`;