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}
-
-
- - Zipcode: {building.zipcode}
- - BBL: {building.bbl}
- - Building ID: {building.building_id}
- - Lot ID: {building.lot_id}
- - Borough: {building.borough}
-
-
- );
+
+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}
+
+
+ - Zipcode: {building.zipcode}
+ - BBL: {building.bbl}
+ - Building ID: {building.building_id}
+ - Lot ID: {building.lot_id}
+ - Borough: {building.borough}
+
+
+
+
+ );
+ }
}
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/`;