diff --git a/src/actions/bmdashboard/consumableActions.js b/src/actions/bmdashboard/consumableActions.js new file mode 100644 index 0000000000..1f002f8975 --- /dev/null +++ b/src/actions/bmdashboard/consumableActions.js @@ -0,0 +1,30 @@ +import axios from "axios"; +import { SET_CONSUMABLES } from "constants/bmdashboard/consumableConstants"; +import { GET_ERRORS } from "constants/errors"; +import { ENDPOINTS } from "utils/URL"; + +export const setConsumables = payload => { + return { + type: SET_CONSUMABLES, + payload + } +} + +export const setErrors = payload => { + return { + type: GET_ERRORS, + payload + } +} + +export const fetchAllConsumables = () => { + return async dispatch => { + axios.get(ENDPOINTS.BM_CONSUMABLES) + .then(res => { + dispatch(setConsumables(res.data)) + }) + .catch(err => { + dispatch(setErrors(err)) + }) + } +} \ No newline at end of file diff --git a/src/components/BMDashboard/Consumables/ConsumablesList/Consumables.css b/src/components/BMDashboard/Consumables/ConsumablesList/Consumables.css new file mode 100644 index 0000000000..fa7eabb583 --- /dev/null +++ b/src/components/BMDashboard/Consumables/ConsumablesList/Consumables.css @@ -0,0 +1,59 @@ +.PageViewContainer { + padding: 0% !important; + +} + +.Page { + background-color: #E8F4F9; + width: 100%; + height: auto; + margin: 0px; + padding: 1rem 2rem; + font-size: 15px; +} + +.Box { + background-color: white; + border-radius: 1rem; + padding: 1.5rem; + height: auto; +} + +.modal-open { + padding-right: 0px !important; +} + +.ModalViewContainer { + overflow-y: auto; + overflow-x: auto; + max-height: 75vh; + font-size: small; +} + +@media (min-width: 1200px) { + .ModalViewContainer { + font-size: medium; + } +} + +.InputsMargin { + margin: 0.2rem; +} + +.cusorpointer { + cursor: pointer; +} + +.BuildingTableHeaderLine { + font-weight: 500; + vertical-align: bottom; + border-bottom: 2px solid #dee2e6; + background-color: #E8F4F9; + border-top: 1px solid #dee2e6; +} + +.BuildingTitle { + font-weight: bold; + margin-bottom: 20px; + text-align: center; +} \ No newline at end of file diff --git a/src/components/BMDashboard/Consumables/ConsumablesList/ConsumablesInputs.jsx b/src/components/BMDashboard/Consumables/ConsumablesList/ConsumablesInputs.jsx new file mode 100644 index 0000000000..0353ba578b --- /dev/null +++ b/src/components/BMDashboard/Consumables/ConsumablesList/ConsumablesInputs.jsx @@ -0,0 +1,101 @@ +import { Label, Form, Row, Col } from 'reactstrap'; +import { useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Select from 'react-select'; +import { fetchBMProjects } from 'actions/bmdashboard/projectActions'; +import './Consumables.css'; + +function ConsumablesInputs({ consumable, setConsumable, project, setProject }) { + const dispatch = useDispatch(); + const projects = useSelector(state => state.bmProjects); + const [formattedProjects, setFormattedProjects] = useState([]); // For React-Select + const [formattedConsumables, setFormattedConsumables] = useState([]); // For React-Select + const consumables = useSelector(state => state.bmConsumables.consumableslist); + + useEffect(() => { + dispatch(fetchBMProjects()); + }, []); + + useEffect(() => { + let _formattedProjects = [{ label: 'All Projects', value: '0' }]; + const tempProjs = projects.map(proj => { + return { label: proj.name, value: proj._id }; + }); + _formattedProjects = _formattedProjects.concat(tempProjs); + setFormattedProjects(_formattedProjects); + }, [projects]); + + useEffect(() => { + let consumablesSet = []; + let _formattedConsumables = [{ label: 'All Consumables', value: '0' }]; + + if (consumables.length) { + if (project.value === '0') + consumablesSet = [...new Set(consumables.map(rec => rec.itemType.name))]; + else + consumablesSet = [ + ...new Set( + consumables + .filter(rec => rec.project?.name === project.label) + .map(rec => rec.itemType.name), + ), + ]; + } + const temp = consumablesSet.map(con => { + return { label: con, value: con }; + }); + _formattedConsumables = _formattedConsumables.concat(temp); + setFormattedConsumables(_formattedConsumables); + }, [consumables, project]); + + const projectHandler = selected => { + setProject(selected); + setConsumable({ label: 'All Consumables', value: '0' }); + }; + + const consumableHandler = selected => { + setConsumable(selected); + }; + + return ( +
+
+ + + + + + + + + + +
+
+ ); +} + +export default ConsumablesInputs; diff --git a/src/components/BMDashboard/Consumables/ConsumablesList/ConsumablesTable.jsx b/src/components/BMDashboard/Consumables/ConsumablesList/ConsumablesTable.jsx new file mode 100644 index 0000000000..05027fdedb --- /dev/null +++ b/src/components/BMDashboard/Consumables/ConsumablesList/ConsumablesTable.jsx @@ -0,0 +1,194 @@ +import { useEffect, useState } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { Table, Button } from 'reactstrap'; +import { BiPencil } from 'react-icons/bi'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSortDown, faSortUp } from '@fortawesome/free-solid-svg-icons'; +import './Consumables.css'; +import ReactTooltip from 'react-tooltip'; +import { fetchAllConsumables } from 'actions/bmdashboard/consumableActions'; +import ConsumablesViewModal from './ConsumablesViewModal'; + +function ConsumablesTable({ consumable, project }) { + // Data fetched in the parent component : ConsumablesView + const dispatch = useDispatch(); + + const consumables = useSelector(state => state.bmConsumables.consumableslist); + const [recordType, setRecordType] = useState(null); + const [modal, setModal] = useState(false); + const [selectedRow, setSelectedRow] = useState(null); + const [sortOrder, setSortOrder] = useState({ project: 'asc', itemType: 'asc' }); + const [iconToDisplay, setIconToDisplay] = useState({ project: faSortUp, itemType: faSortUp }); + const [consumablesViewData, setConsumablesViewData] = useState(null); + + useEffect(() => { + dispatch(fetchAllConsumables()); + }, []); + + useEffect(() => { + setConsumablesViewData(consumables); + }, [consumables]); + + const handleSort = column => { + if (!column || consumables.length === 0) return; + switch (column) { + case 'project': { + setSortOrder({ ...sortOrder, project: sortOrder.project === 'asc' ? 'desc' : 'asc' }); + setIconToDisplay({ + ...iconToDisplay, + project: iconToDisplay.project === faSortUp ? faSortDown : faSortUp, + }); + const factor = sortOrder.project === 'asc' ? 1 : -1; + const _consumablesViewData = [...consumables].sort((a, b) => { + return factor * a.project.name.localeCompare(b.project.name); + }); + setConsumablesViewData(_consumablesViewData); + break; + } + case 'itemType': { + setSortOrder({ ...sortOrder, itemType: sortOrder.itemType === 'asc' ? 'desc' : 'asc' }); + setIconToDisplay({ + ...iconToDisplay, + itemType: iconToDisplay.itemType === faSortUp ? faSortDown : faSortUp, + }); + const factor = sortOrder.itemType === 'asc' ? 1 : -1; + const _consumablesViewData = [...consumables].sort((a, b) => { + return factor * a.itemType.name.localeCompare(b.itemType.name); + }); + setConsumablesViewData(_consumablesViewData); + break; + } + default: { + break; + } + } + }; + + const handleOpenModal = (row, type) => { + setSelectedRow(row); // current row data + setRecordType(type); // UpdatesEdit/UpdatesView/PurchasesEdit/PurchasesView + setModal(true); + }; + + useEffect(() => { + if (project.value !== '0') { + const _consumables = consumables.filter(rec => rec.project?.name === project.label); + setConsumablesViewData(_consumables); + } else { + setConsumablesViewData([...consumables]); + } + }, [project]); + + useEffect(() => { + let _consumables; + if (project.value === '0' && consumable.value === '0') { + setConsumablesViewData([...consumables]); + } else if (project.value !== '0' && consumable.value === '0') { + _consumables = consumables.filter(rec => rec.project?._id === project.value); + setConsumablesViewData([..._consumables]); + } else if (project.value === '0' && consumable.value !== '0') { + _consumables = consumables.filter(rec => rec.itemType?.name === consumable.value); + setConsumablesViewData([..._consumables]); + } else { + _consumables = consumables.filter( + rec => rec.project?._id === project.value && rec.itemType?.name === consumable.value, + ); + setConsumablesViewData([..._consumables]); + } + }, [project, consumable]); + + return ( +
+
+ + + + + + + + + + + + + + + + + {consumablesViewData && consumablesViewData.length > 0 ? ( + consumablesViewData.map(rec => { + return ( + + + + + + + + + + + + + ); + }) + ) : ( + + + + )} + +
handleSort('project')}> +
+
Project
+ +
+ +
handleSort('itemType')}> +
+
Name
+ +
+ +
UnitBoughtUsedAvailableWasteUpdatesPurchases
{rec.project?.name}{rec.itemType?.name}{rec.itemType?.unit}{rec.stockBought}{rec.stockUsed}{rec.stockAvailable}{rec.stockWasted} + + + + +
+ No consumables data available +
+
+
+ ); +} + +export default ConsumablesTable; diff --git a/src/components/BMDashboard/Consumables/ConsumablesList/ConsumablesView.jsx b/src/components/BMDashboard/Consumables/ConsumablesList/ConsumablesView.jsx new file mode 100644 index 0000000000..307c3ea314 --- /dev/null +++ b/src/components/BMDashboard/Consumables/ConsumablesList/ConsumablesView.jsx @@ -0,0 +1,33 @@ +import { useState } from 'react'; +import ConsumablesTable from './ConsumablesTable'; +import ConsumablesInputs from './ConsumablesInputs'; +import './Consumables.css'; + +function ConsumablesView() { + const [consumable, setConsumable] = useState({ label: 'All Consumables', value: '0' }); + const [project, setProject] = useState({ label: 'All Projects', value: '0' }); + + return ( +
+
+
+
CONSUMABLES
+ + +
+
+
+ ); +} + +export default ConsumablesView; diff --git a/src/components/BMDashboard/Consumables/ConsumablesList/ConsumablesViewModal.jsx b/src/components/BMDashboard/Consumables/ConsumablesList/ConsumablesViewModal.jsx new file mode 100644 index 0000000000..fb40915fc7 --- /dev/null +++ b/src/components/BMDashboard/Consumables/ConsumablesList/ConsumablesViewModal.jsx @@ -0,0 +1,100 @@ +import { Modal, ModalHeader, ModalBody, ModalFooter, Button, Table } from 'reactstrap'; +import './Consumables.css'; +import moment from 'moment'; + +function ConsumablesViewModal({ modal, setModal, record, recordType }) { + if (record) { + const toggle = () => { + setModal(false); + }; + + return ( + + + Consumables   + {recordType === 'UpdatesView' && 'Update History'} + {recordType === 'PurchasesView' && 'Purchase History'} + {recordType === 'UpdatesEdit' && 'Edit Record'} + + +
+ + +
+
+
+ + + +
+ ); + } + return null; +} + +export default ConsumablesViewModal; + +function Record({ record, recordType }) { + if (recordType === 'UpdatesView') { + return ( + <> + + + Date + Quantity Used + Quantity Wasted + Creator + + + + {record.updateRecord.map(data => { + return ( + + {moment.utc(data.date).format('LL')} + {`${data.quantityUsed} ${record.itemType?.unit}` || '-'} + {`${data.quantityWasted} ${record.itemType?.unit}` || '-'} + + + {`${data.createdBy.firstName} ${data.createdBy.lastName}`} + + + + ); + })} + + + ); + } + if (recordType === 'PurchasesView') { + return ( + <> + + + Date + Status + Quantity + Creator + + + + {record.purchaseRecord.map(data => { + return ( + + {moment.utc(data.date).format('LL')} + {`${data.quantityUsed} ${record.itemType?.unit}` || '-'} + {`${data.quantityWasted} ${record.itemType?.unit}` || '-'} + + + {`${data.requestedBy.firstName} ${data.requestedBy.lastName}`} + + + + ); + })} + + + ); + } + + return null; +} diff --git a/src/components/BMDashboard/MaterialsList/RecordsModal.css b/src/components/BMDashboard/MaterialsList/RecordsModal.css index 413c8a2c12..a8d4e31069 100644 --- a/src/components/BMDashboard/MaterialsList/RecordsModal.css +++ b/src/components/BMDashboard/MaterialsList/RecordsModal.css @@ -10,4 +10,4 @@ .records_modal_table_container { font-size: medium; } -} +} \ No newline at end of file diff --git a/src/constants/bmdashboard/consumableConstants.js b/src/constants/bmdashboard/consumableConstants.js new file mode 100644 index 0000000000..5db8979b63 --- /dev/null +++ b/src/constants/bmdashboard/consumableConstants.js @@ -0,0 +1,2 @@ +export const SET_CONSUMABLES = 'SET_CONSUMABLES'; +export default SET_CONSUMABLES; diff --git a/src/reducers/bmdashboard/consumablesReducer.js b/src/reducers/bmdashboard/consumablesReducer.js new file mode 100644 index 0000000000..49b693b82f --- /dev/null +++ b/src/reducers/bmdashboard/consumablesReducer.js @@ -0,0 +1,32 @@ +import { + SET_CONSUMABLES +} from "constants/bmdashboard/consumableConstants" + +const defaultState = { + consumableslist: [], + updateConsumables: { + loading: false, + result: null, + error: undefined + }, + updateConsumablesBulk: { + loading: false, + result: null, + error: undefined + } +} + +export const consumablesReducer = (consumables = defaultState, action) => { + switch (action.type) { + case SET_CONSUMABLES: + { + consumables.consumableslist = action.payload; + return { + ...consumables, updateConsumables: { ...defaultState.updateConsumables }, + updateConsumablesBulk: { ...defaultState.updateConsumablesBulk } + } + } + default: + return consumables; + } +} \ No newline at end of file diff --git a/src/reducers/index.js b/src/reducers/index.js index 59886f646c..f155f07f00 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -33,6 +33,7 @@ import { mouseoverTextReducer } from './mouseoverTextReducer'; import { materialsReducer } from './bmdashboard/materialsReducer'; import { bmProjectReducer } from './bmdashboard/projectReducer'; import { bmInvTypeReducer } from './bmdashboard/inventoryTypeReducer'; +import { consumablesReducer } from './bmdashboard/consumablesReducer'; export default combineReducers({ auth: authReducer, @@ -69,5 +70,6 @@ export default combineReducers({ // bmdashboard materials: materialsReducer, bmProjects: bmProjectReducer, - bmInvTypes: bmInvTypeReducer + bmInvTypes: bmInvTypeReducer, + bmConsumables: consumablesReducer }); diff --git a/src/routes.js b/src/routes.js index e9e61f6bfc..6ac9dfc053 100644 --- a/src/routes.js +++ b/src/routes.js @@ -11,6 +11,7 @@ import { RoutePermissions } from 'utils/routePermissions'; // import UserRoleTab from 'components/PermissionsManagement/UserRoleTab'; import EditableInfoModal from 'components/UserProfile/EditableModal/EditableInfoModal'; import RoleInfoCollections from 'components/UserProfile/EditableModal/roleInfoModal'; + import AddEquipmentType from 'components/BMDashboard/Equipment/Add/AddEquipmentType'; import Timelog from './components/Timelog'; import LessonForm from './components/BMDashboard/Lesson/LessonForm'; @@ -43,10 +44,13 @@ import ForgotPassword from './components/Login/ForgotPassword'; import Inventory from './components/Inventory'; // import BadgeManagement from './components/Badge/BadgeManagement'; + // BM Dashboard import BMProtectedRoute from './components/common/BMDashboard/BMProtectedRoute'; import BMDashboard from './components/BMDashboard'; import BMLogin from './components/BMDashboard/Login'; +import ConsumablesView from './components/BMDashboard/Consumables/ConsumablesList/ConsumablesView'; + import EquipmentList from './components/BMDashboard/Equipment/List'; // import MaterialsList from './components/BMDashboard/MaterialsList'; // import PurchaseMaterials from './components/BMDashboard/MaterialPurchaseRequest'; @@ -206,12 +210,14 @@ export default ( - - + - + + + + {/* Temporary route to redirect all subdirectories to login if unauthenticated */} diff --git a/src/utils/URL.js b/src/utils/URL.js index be5492feeb..0c4fcbf6c5 100644 --- a/src/utils/URL.js +++ b/src/utils/URL.js @@ -129,6 +129,7 @@ export const ENDPOINTS = { BM_LOGIN: `${APIEndpoint}/bm/login`, BM_MATERIAL_TYPES: `${APIEndpoint}/bm/invtypes/materials`, BM_MATERIALS: `${APIEndpoint}/bm/materials`, + BM_CONSUMABLES: `${APIEndpoint}/bm/consumables`, BM_PROJECTS: `${APIEndpoint}/bm/projects`, BM_UPDATE_MATERIAL: `${APIEndpoint}/bm/updateMaterialRecord`, BM_UPDATE_MATERIAL_BULK: `${APIEndpoint}/bm/updateMaterialRecordBulk`,