diff --git a/client/src/scenes/UploadDLockData/components/FileUpload.js b/client/src/scenes/UploadDLockData/components/FileUpload.js index c217294..87b1038 100644 --- a/client/src/scenes/UploadDLockData/components/FileUpload.js +++ b/client/src/scenes/UploadDLockData/components/FileUpload.js @@ -1,8 +1,10 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; -import {Form} from "semantic-ui-react"; +import { Form } from "semantic-ui-react"; -import { uploadDoorLockData } from "../../../store/actions"; +import UnknownMapping from './UnknownMapping'; + +import { uploadDoorLockData, fetchMappings } from "../../../store/actions"; class FileUpload extends Component { constructor(props) { @@ -10,15 +12,68 @@ class FileUpload extends Component { this.state = { files: null, + unknownMappings: [], }; this.onFileChange = this.onFileChange.bind(this); this.onUploadClick = this.onUploadClick.bind(this); } + componentDidMount() { + const { fetchMappings } = this.props; + fetchMappings(); + } + + componentWillReceiveProps(nextProps, nextContext) { + const { addedMapping } = nextProps; + const { unknownMappings } = this.state; + + const filteredUnknownMappings = unknownMappings.filter(mapping => { + return mapping.officeSlug !== addedMapping.officeSlug + || mapping.resourceSlug !== addedMapping.resourceSlug + }); + + this.setState({unknownMappings: filteredUnknownMappings}); + } + + extractMappingFromFileName(fileName) { + const contentBetweenBracketsRegex = /\[(.*?)\]/; + const rawContent = fileName.match(contentBetweenBracketsRegex)[1]; + const mappingContent = rawContent.split('-').map(word => word.trim()); + return { + officeSlug: mappingContent[0], + resourceSlug: mappingContent[1], + } + } + + checkIfMappingExsists(mappingFromFileName) { + const { mappings } = this.props; + const { officeSlug, resourceSlug } = mappingFromFileName; + + const { existingMappings } = mappings; + + return existingMappings.find(mapping => (mapping.officeSlug === officeSlug) && (mapping.resourceSlug === resourceSlug)); + } + onFileChange(event) { const files = event.target.files; - this.setState({files}); + const unknownMappings = []; + + + Array.from(files).forEach(file => { + const mappingFromFileName = this.extractMappingFromFileName(file.name); + if (!this.checkIfMappingExsists(mappingFromFileName)){ + unknownMappings.push({ + file: file.name, + officeId: null, + resourceId: null, + officeSlug: mappingFromFileName.officeSlug, + resourceSlug: mappingFromFileName.resourceSlug, + }) + } + }); + + this.setState({files, unknownMappings}); }; onUploadClick() { @@ -31,7 +86,11 @@ class FileUpload extends Component { }; render() { - const { pending } = this.props; + const { pendingUpload } = this.props; + const { unknownMappings, files } = this.state; + + const uploadDisabled = pendingUpload || unknownMappings.length > 0 || !files; + return (
- Upload + { + unknownMappings.map((mapping, index) => + ) + } +
+ Upload
); } } const mapStateToProps = (state) => ({ - pending: state.doorLockData.pending, + pendingUpload: state.doorLockData.pending, + pendingMappings: state.mappingsData.pending, + mappings: state.mappingsData.result, + addedMapping: state.addMapping.result, }); const mapDispatchToProps = (dispatch) => ({ - uploadDoorLockData: (doorLockDataFiles) => uploadDoorLockData(dispatch, doorLockDataFiles) + uploadDoorLockData: (doorLockDataFiles) => uploadDoorLockData(dispatch, doorLockDataFiles), + fetchMappings: () => fetchMappings(dispatch), }); export default connect(mapStateToProps, mapDispatchToProps)(FileUpload); diff --git a/client/src/scenes/UploadDLockData/components/UnknownMapping.js b/client/src/scenes/UploadDLockData/components/UnknownMapping.js new file mode 100644 index 0000000..3bee93e --- /dev/null +++ b/client/src/scenes/UploadDLockData/components/UnknownMapping.js @@ -0,0 +1,172 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import {Button, Dropdown, Message} from "semantic-ui-react"; +import Fuse from 'fuse.js'; +import { addNewMapping } from "../../../store/actions"; + +class UnknownMapping extends Component { + constructor(props) { + super(props); + + const guessedValues = this.guessDropdownValues(this.props); + + this.state = { + selectedOfficeId: guessedValues.officeValue, + selectedResourceId: guessedValues.resourceValue, + } + } + + componentWillReceiveProps(nextProps, nextContext) { + const guessedValues = this.guessDropdownValues(nextProps); + this.setState({selectedOfficeId: guessedValues.officeValue, selectedResourceId: guessedValues.resourceValue}); + } + + guessDropdownValues(props){ + const { mappings, mapping } = props; + + const offices = mappings && mappings.offices ? mappings.offices : []; + const resources = mappings && mappings.resources ? mappings.resources : []; + + const fuzzySearchOptions = { + shouldSort: true, + threshold: 0.5, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + keys: [ + "officeName", + "resourceName", + ] + }; + + const officesFuse = new Fuse(offices, fuzzySearchOptions); + const fuzzyOfficeSearchResults = officesFuse.search(mapping.officeSlug); + + let officeValue = null; + if (fuzzyOfficeSearchResults.length > 0){ + officeValue = fuzzyOfficeSearchResults[0].officeId; + }else if (offices.length > 0){ + officeValue = offices[0].officeId; + } + + const filteredResources = resources.filter(resource => resource.officeId === officeValue); + + const resourcesFuse = new Fuse(filteredResources, fuzzySearchOptions); + const fuzzyResourcesSearchResults = resourcesFuse.search(mapping.resourceSlug); + + let resourceValue = null; + if (fuzzyResourcesSearchResults.length > 0){ + resourceValue = fuzzyResourcesSearchResults[0].resourceId; + }else if (filteredResources.length > 0){ + resourceValue = filteredResources[0].resourceId; + } + + return { + officeValue, + resourceValue, + } + } + + onOfficeChange(event, data) { + const { mappings } = this.props; + + const selectedOfficeId = data.value || null; + const resources = mappings && mappings.resources ? mappings.resources : []; + const filteredResources = resources.filter(resource => resource.officeId === selectedOfficeId); + const selectedResourceId = filteredResources.length > 0 ? filteredResources[0].resourceId : null; + this.setState({selectedOfficeId, selectedResourceId}); + } + + onResourceChange(event, data) { + const selectedResourceId = data.value || null; + this.setState({selectedResourceId}); + } + + onSave(){ + const { addNewMapping, mapping } = this.props; + const { selectedOfficeId, selectedResourceId } = this.state; + const officeSlug = mapping.officeSlug; + const resourceSlug = mapping.resourceSlug; + + const newMapping = { + officeSlug, + resourceSlug, + officeId: selectedOfficeId, + resourceId: selectedResourceId, + }; + + addNewMapping(newMapping); + } + + render() { + const { mapping, mappings } = this.props; + const { selectedOfficeId, selectedResourceId } = this.state; + + const offices = mappings && mappings.offices ? mappings.offices : []; + const resources = mappings && mappings.resources ? mappings.resources : []; + + const officeDropdownOptions = offices.map(office => { + return { + key: office.officeId, + value: office.officeId, + text: office.officeName, + } + }); + + const filteredResources = resources.filter(resource => resource.officeId === selectedOfficeId); + + const resourceDropdownOptions = filteredResources.map(resource => { + return { + key: resource.resourceId, + value: resource.resourceId, + text: resource.resourceName, + } + }); + + const saveButtonDisabled = !selectedOfficeId || !selectedResourceId; + + return ( +
+
+ + {mapping.file} +
+ + This file contains the unknown location. Based on ORD data, it seems that this file is related to {' '} + + {' '} + / + {' '} + + +
+ +
+
); + } +} + +const mapStateToProps = (state) => ({ + mappings: state.mappingsData.result, +}); + +const mapDispatchToProps = (dispatch) => ({ + addNewMapping: (mapping) => addNewMapping(dispatch, mapping), +}); + + +export default connect(mapStateToProps, mapDispatchToProps)(UnknownMapping); diff --git a/client/src/store/actions/index.js b/client/src/store/actions/index.js index d21e20e..4415670 100644 --- a/client/src/store/actions/index.js +++ b/client/src/store/actions/index.js @@ -1 +1,2 @@ export * from './doorLockActions'; +export * from './officeRnDActions'; diff --git a/client/src/store/actions/officeRnDActions.js b/client/src/store/actions/officeRnDActions.js new file mode 100644 index 0000000..f858967 --- /dev/null +++ b/client/src/store/actions/officeRnDActions.js @@ -0,0 +1,34 @@ +import { + FETCH_MAPPINGS_PENDING, + FETCH_MAPPINGS_SUCCESS, + FETCH_MAPPINGS_FAILED, + ADD_NEW_MAPPING_PENDING, + ADD_NEW_MAPPING_SUCCESS, + ADD_NEW_MAPPING_FAILED, +} from "../constants"; + +import API from '../../utilities/api'; + +export const fetchMappings = (dispatch) => { + dispatch({type: FETCH_MAPPINGS_PENDING}); + API.get('doorLock/mappings') + .then(response => { + dispatch({type: FETCH_MAPPINGS_SUCCESS, payload: response.data}); + }) + .catch(error => { + dispatch({type: FETCH_MAPPINGS_FAILED, payload: error.response}); + }); +}; + +export const addNewMapping = (dispatch, mapping) => { + dispatch({type: ADD_NEW_MAPPING_PENDING}); + API.post('doorLock/mappings', { + mapping + }) + .then(response => { + dispatch({type: ADD_NEW_MAPPING_SUCCESS, payload: response.data}); + }) + .catch(error => { + dispatch({type: ADD_NEW_MAPPING_FAILED, payload: error.response}); + }); +}; diff --git a/client/src/store/reducers/addMappingReducer.js b/client/src/store/reducers/addMappingReducer.js new file mode 100644 index 0000000..c360882 --- /dev/null +++ b/client/src/store/reducers/addMappingReducer.js @@ -0,0 +1,38 @@ +import { + ADD_NEW_MAPPING_PENDING, + ADD_NEW_MAPPING_SUCCESS, + ADD_NEW_MAPPING_FAILED, +} from "../constants"; + +const initialState = { + pending: false, + result: null, + error: null, +}; + +export const addMapping = (state, action) => { + state = state || initialState; + action = action || {}; + + switch(action.type){ + case ADD_NEW_MAPPING_PENDING: + return Object.assign({}, state, { + pending: true, + error: null, + }); + case ADD_NEW_MAPPING_SUCCESS: + return Object.assign({}, state, { + pending: false, + result: action.payload, + error: null, + }); + case ADD_NEW_MAPPING_FAILED: + return Object.assign({}, state, { + pending: false, + result: {}, + error: action.payload, + }); + default: + return state; + } +}; diff --git a/client/src/store/reducers/index.js b/client/src/store/reducers/index.js index 274b7bd..ed2a547 100644 --- a/client/src/store/reducers/index.js +++ b/client/src/store/reducers/index.js @@ -1,8 +1,12 @@ import { combineReducers } from "redux"; import { doorLockData} from "./doorLockReducers"; +import { mappingsData } from "./mappingsReducer"; +import { addMapping } from './addMappingReducer'; export const rootReducer = combineReducers({ - doorLockData + doorLockData, + mappingsData, + addMapping, }); diff --git a/client/src/store/reducers/mappingsReducer.js b/client/src/store/reducers/mappingsReducer.js new file mode 100644 index 0000000..95e0d9d --- /dev/null +++ b/client/src/store/reducers/mappingsReducer.js @@ -0,0 +1,38 @@ +import { + FETCH_MAPPINGS_PENDING, + FETCH_MAPPINGS_SUCCESS, + FETCH_MAPPINGS_FAILED, +} from "../constants"; + +const initialState = { + pending: false, + result: null, + error: null, +}; + +export const mappingsData = (state, action) => { + state = state || initialState; + action = action || {}; + + switch(action.type){ + case FETCH_MAPPINGS_PENDING: + return Object.assign({}, state, { + pending: true, + error: null, + }); + case FETCH_MAPPINGS_SUCCESS: + return Object.assign({}, state, { + pending: false, + result: action.payload, + error: null, + }); + case FETCH_MAPPINGS_FAILED: + return Object.assign({}, state, { + pending: false, + result: {}, + error: action.payload, + }); + default: + return state; + } +};