Fixes/ll inventory (#70)

* inventory data changes
* update Inventory changes
* Fixed: removed unnecessary imports
* Disabled: cycle count
* Added: Inventory types sagas
* Fix: null check
* Updated: policies
* Fixed: formik values
* update: allow single image
* Update: Policies control
* Updated: new inventory add form
* Update: new inventory conditional render
* Update: populate formik fields
* Added: Validation
* Added: edit functionality, disabled fields
* Update: housekeeping
* Fix: iconslug and key
* Update: route handling
* Added: endpoints
* Added: widget nested page
* Added: sagas
* Added: redux handling
* Update: new product page functionality
* Added: inventory page functionality
* Fixed: form validation
* Fix: route handling
* Added: Add items button
* Added: Item list page
* Added: saga
* Added: Route handler
* Added: item redux
* Udpate: breadcrumbs
* Fixed: edit widget
* Fix: delete widget
* Added: item list table
* Update: Routing
* Update: dynamic headers
* Update: disabled edit
* Added: toastify and toast
* removed: nested table page
* Fixewd: switch and inventory form
* Update: data table
* Fixed: image upload
* Update: redux and widget
* Update: warehouse edit with selector

Co-authored-by: evdigitech <evdigitech@gmail.com>
Co-authored-by: Llewellyn Dsouza <lledsouza2209@gmail.com>
This commit is contained in:
bluestreamlds
2022-03-01 23:47:12 +05:30
committed by GitHub
parent af28c0b99c
commit 315113630e
22 changed files with 782 additions and 479 deletions

View File

@@ -53,6 +53,9 @@ import reduxStore from './redux/Store';
import { protectedRoutes as routes } from './routes/index';
import PrivateRoute from './routes/PrivateRoute';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
export default function App() {
const [controller, dispatch] = useMaterialUIController();
const {
@@ -166,6 +169,7 @@ export default function App() {
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</ThemeProvider>
<ToastContainer />
</PersistGate>
</Provider>
);

View File

@@ -0,0 +1,283 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import PropTypes from 'prop-types';
import SearchBar from 'components/SearchBar';
import MDButton from 'components/Button';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Fade from '@mui/material/Fade';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import { styled } from '@mui/material/styles';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell, { tableCellClasses } from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
// import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
// import IconButton from '@mui/material/IconButton';
// import Collapse from '@mui/material/Collapse';
import TablePagination from 'components/TablePagination';
const StyledTableCell = styled(TableCell)(({ theme }) => ({
[`&.${tableCellClasses.head}`]: {
backgroundColor: '#e5e7eb',
color: theme.palette.common.black,
fontWeight: 400
},
[`&.${tableCellClasses.body}`]: {
fontSize: 14,
fontWeight: 400
}
}));
const StyledTableRow = styled(TableRow)(({ theme }) => ({
'&:nth-of-type(2n+1)': {
backgroundColor: theme.palette.action.hover
},
'td, th': {
padding: '0.75rem 0.5rem'
},
// hide last border
'&:last-child td, &:last-child th': {
border: 0
}
}));
Row.propTypes = {
rowData: PropTypes.array,
tHeads: PropTypes.array
};
function Row({ tHeads, rowData }) {
return (
<React.Fragment>
<StyledTableRow sx={{ '&odd > *': { borderBottom: 'unset' } }}>
<StyledTableCell sx={{ width: '10%', display: 'flex', alignItems: 'center' }}>
<MDButton
disabled
size="small"
variant="contained"
color="primary"
sx={{
textTransform: 'capitalize',
minWidth: '45px',
minHeight: '28px',
marginLeft: '10px',
boxShadow: 'none',
fontWeight: '500',
padding: '0'
}}
>
EDIT
</MDButton>
</StyledTableCell>
{tHeads &&
tHeads
.slice(1)
.map((head) => <StyledTableCell key={head.key}>{rowData[head.key]}</StyledTableCell>)}
</StyledTableRow>
</React.Fragment>
);
}
function EnhancedTable({ data, tHeads }) {
// const [anchorEl, setAnchorEl] = React.useState(false);
// const open = Boolean(anchorEl);
// const handleClick = (event) => {
// setAnchorEl(event.currentTarget);
// };
// const handleClose = () => {
// setAnchorEl(null);
// };
return (
<>
<Box
sx={{
border: '1px solid #c4c4c4',
borderTop: '6px solid #007aff',
borderRadius: '4px',
overflow: 'hidden'
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '16px'
}}
>
<Box>
<SearchBar />
</Box>
<Box sx={{ display: 'flex', columnGap: '15px' }}>
<MDButton
size="small"
variant="outlined"
color="primary"
sx={{
textTransform: 'capitalize',
minWidth: '60px',
minHeight: '44px',
fontWeight: '500'
}}
>
Sorting
</MDButton>
<MDButton
id="fade-button"
size="small"
variant="outlined"
color="primary"
sx={{
textTransform: 'capitalize',
minWidth: '60px',
minHeight: '44px',
fontWeight: '500'
}}
aria-controls={open ? 'fade-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
endIcon={<KeyboardArrowDownIcon />}
onClick={() => {}}
>
Dashboard
</MDButton>
<Menu
id="fade-menu"
MenuListProps={{
'aria-labelledby': 'fade-button'
}}
anchorEl={null}
open={false}
TransitionComponent={Fade}
onClose={() => {}}
>
<MenuItem>Profile</MenuItem>
<MenuItem>My account</MenuItem>
<MenuItem>Logout</MenuItem>
</Menu>
</Box>
</Box>
{/* Table-row- */}
<TableContainer component={Paper} sx={{ borderRadius: '0 !important', boxShadow: 'none' }}>
<Table aria-label="collapsible table" sx={{ minWidth: 700 }}>
<TableHead sx={{ display: 'table-header-group' }}>
<TableRow>
{tHeads &&
tHeads.map((head) => (
<StyledTableCell key={head.name}>{head.name}</StyledTableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{data &&
data.map((rowData) => <Row key={rowData._id} rowData={rowData} tHeads={tHeads} />)}
</TableBody>
</Table>
</TableContainer>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '15px 10px'
}}
>
{/* <Box sx={{ display: 'flex', alignItems: 'center' }}>
<MDButton
size="small"
variant="outlined"
color="primary"
sx={{
textTransform: 'inherit',
minWidth: '45px',
minHeight: '28px',
boxShadow: 'none',
fontWeight: '500',
padding: '0'
}}
>
Go to
</MDButton>
<MDButton
size="small"
variant="outlined"
color="primary"
sx={{
textTransform: 'inherit',
minWidth: '45px',
minHeight: '28px',
marginLeft: '10px',
boxShadow: 'none',
fontWeight: '500',
padding: '0',
border: ' 1px solid #C2C2C2',
color: '#000'
}}
>
1
</MDButton>
<Box
sx={{ display: 'flex', alignItems: 'center', fontSize: '12px', marginLeft: '15px' }}
>
View:
<MDButton
id="fade-button"
size="small"
variant="outlined"
color="primary"
aria-controls={open ? 'fade-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
endIcon={<KeyboardArrowDownIcon />}
sx={{
textTransform: 'inherit',
minWidth: '45px',
minHeight: '28px',
marginLeft: '10px',
boxShadow: 'none',
fontWeight: '500',
padding: '0',
border: ' 1px solid #C2C2C2',
color: '#000'
}}
onClick={handleClick}
>
12
</MDButton>
<Menu
id="fade-menu"
MenuListProps={{
'aria-labelledby': 'fade-button'
}}
anchorEl={anchorEl}
open={open}
TransitionComponent={Fade}
onClose={handleClose}
>
<MenuItem>Profile</MenuItem>
<MenuItem>My account</MenuItem>
<MenuItem>Logout</MenuItem>
</Menu>
</Box>
</Box> */}
{/*---- pagination- */}
<Box>
<TablePagination />
</Box>
<Box sx={{ fontSize: '14px', color: '#000' }}>[1 to 10 of 92]</Box>
</Box>
</Box>
</>
);
}
EnhancedTable.propTypes = {
data: PropTypes.array,
tHeads: PropTypes.array
};
export default EnhancedTable;

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-case-declarations */
import MDBox from 'components/MDBox';
import PropTypes from 'prop-types';
import UploadIcon from 'assets/images/UploadIcon';
@@ -5,15 +6,48 @@ import MDTypography from 'components/MDTypography';
import pxToRem from 'assets/theme-dark/functions/pxToRem';
import { Button } from '@mui/material';
import Close from 'assets/images/Close';
import { AuthorizedAPI } from 'config';
import LOGGER from 'services/Logger';
function ImageUpload({ heading, accept, multiple, images, setImages }) {
const deleteImage = async (type, id, imageId) => {
LOGGER.log({ type, id, imageId });
switch (type) {
case 'warehouse':
await AuthorizedAPI.delete(`/warehouse/${id}/image/${imageId}`);
break;
default:
break;
}
};
const addNewImage = async (type, id, image, addNewImageToImages) => {
LOGGER.log({ type, id, image });
const formData = new FormData();
switch (type) {
case 'warehouse':
formData.append('warehouse-image', image);
const response = await AuthorizedAPI.post(`/warehouse/add-image/${id}`, formData);
addNewImageToImages(response?.data?.data);
return;
default:
break;
}
};
function ImageUpload({ heading, accept, multiple, images, setImages, type, pageId }) {
const addNewImageToImages = (image) => {
setImages([...images, image]);
};
const addImage = (e) => {
addNewImage(type, pageId, e.target.files[0], addNewImageToImages);
setImages([
...images,
{ src: URL.createObjectURL(e.target.files[0]), file: e.target.files[0] }
]);
};
const removeImage = (index) => {
deleteImage(type, pageId, images._id);
setImages(images.filter((_val, idx) => idx !== index));
};
@@ -26,91 +60,133 @@ function ImageUpload({ heading, accept, multiple, images, setImages }) {
padding: pxToRem(16)
}}
>
<MDBox
sx={{
border: '1px dashed #C4C4C4',
borderRadius: pxToRem(6),
cursor: 'pointer',
position: 'relative',
textAlign: 'center',
minHeight: pxToRem(200),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '16px'
}}
>
{multiple || !images.length ? (
<MDBox
component="input"
name="file"
disabled={!multiple && images.length}
accept={accept}
type="file"
sx={{
width: '100%',
opacity: '0',
border: '1px dashed #C4C4C4',
borderRadius: pxToRem(6),
cursor: 'pointer',
position: 'absolute',
top: '0',
left: '0',
right: '0',
bottom: '0'
position: 'relative',
textAlign: 'center',
minHeight: pxToRem(200),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '16px'
}}
onChange={addImage}
/>
<MDBox component="span">
{!multiple && images.length ? null : <UploadIcon />}
<MDTypography
component="span"
sx={{ color: '#000', letterSpacing: '0.01em', display: 'block' }}
>
{!multiple && images.length ? 'Cannot add more images' : heading}
</MDTypography>
>
<MDBox
component="input"
name="file"
disabled={!multiple && images.length}
accept={accept}
type="file"
sx={{
width: '100%',
opacity: '0',
cursor: 'pointer',
position: 'absolute',
top: '0',
left: '0',
right: '0',
bottom: '0'
}}
onChange={addImage}
/>
<MDBox component="span">
{!multiple && images.length ? null : <UploadIcon />}
<MDTypography
component="span"
sx={{ color: '#000', letterSpacing: '0.01em', display: 'block' }}
>
{!multiple && images.length ? 'Cannot add more images' : heading}
</MDTypography>
</MDBox>
</MDBox>
</MDBox>
) : (
<MDBox
sx={{
border: '1px dashed #C4C4C4',
borderRadius: pxToRem(6),
cursor: 'pointer',
position: 'relative',
textAlign: 'center',
minHeight: pxToRem(200),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: '16px'
}}
>
<img src={images[0].src} alt="" width="100%" height="100%" />
<Button
sx={{
backgroundColor: '#fff !important',
boxShadow: '0px 1px 1px rgb(0 0 0 / 25%)',
padding: '0',
minWidth: '20px',
minHeight: '20px',
borderRadius: '100%',
position: 'absolute',
right: '4px',
top: '4px',
'&:hover': {
backgroundColor: 'red !important'
}
}}
onClick={() => {
removeImage(0);
}}
>
<Close />
</Button>
</MDBox>
)}
{/* -----------img-preview----------- */}
<MDBox sx={{ marginBottom: '-10px' }}>
{images &&
images.map((item, idx) => {
return (
<MDBox
key={idx}
component="span"
sx={{
width: '80px',
height: '63px',
marginRight: '16px',
display: 'inline-block',
borderRadius: '4px',
position: 'relative'
}}
>
<img src={item.src} alt="placeholder" width="100%" />
<Button
{multiple ? (
<MDBox sx={{ marginBottom: '-10px' }}>
{images &&
images.map((item, idx) => {
return (
<MDBox
key={idx}
component="span"
sx={{
backgroundColor: '#fff !important',
boxShadow: '0px 1px 1px rgb(0 0 0 / 25%)',
padding: '0',
minWidth: '20px',
minHeight: '20px',
borderRadius: '100%',
position: 'absolute',
right: '4px',
top: '4px',
'&:hover': {
backgroundColor: 'red !important'
}
}}
onClick={() => {
removeImage(idx);
width: '80px',
height: '63px',
marginRight: '16px',
display: 'inline-block',
borderRadius: '4px',
position: 'relative'
}}
>
<Close />
</Button>
</MDBox>
);
})}
</MDBox>
<img src={item.src} alt="placeholder" width="100%" />
<Button
sx={{
backgroundColor: '#fff !important',
boxShadow: '0px 1px 1px rgb(0 0 0 / 25%)',
padding: '0',
minWidth: '20px',
minHeight: '20px',
borderRadius: '100%',
position: 'absolute',
right: '4px',
top: '4px',
'&:hover': {
backgroundColor: 'red !important'
}
}}
onClick={() => {
removeImage(idx);
}}
>
<Close />
</Button>
</MDBox>
);
})}
</MDBox>
) : null}
</MDBox>
</>
);
@@ -122,5 +198,7 @@ ImageUpload.propTypes = {
heading: PropTypes.string,
multiple: PropTypes.bool,
accept: PropTypes.string,
setImages: PropTypes.func
setImages: PropTypes.func,
type: PropTypes.string,
pageId: PropTypes.string
};

View File

@@ -1,8 +1,8 @@
import * as React from 'react';
import FormControlLabel from '@mui/material/FormControlLabel';
// import FormControlLabel from '@mui/material/FormControlLabel';
import PropTypes from 'prop-types';
import { styled } from '@mui/material/styles';
import FormGroup from '@mui/material/FormGroup';
// import FormGroup from '@mui/material/FormGroup';
import SwitchButton from '@mui/material/Switch';
const IOSSwitch = styled((props) => (
@@ -56,14 +56,12 @@ const IOSSwitch = styled((props) => (
}
}));
export default function Switch({ checked }) {
return (
<FormGroup>
<FormControlLabel control={<IOSSwitch defaultChecked={checked} sx={{ m: 1 }} />} label="" />
</FormGroup>
);
export default function Switch({ checked, onChange, name }) {
return <IOSSwitch checked={checked} sx={{ m: 1 }} name={name} onChange={onChange} />;
}
Switch.propTypes = {
checked: PropTypes.bool
checked: PropTypes.any,
name: PropTypes.any,
onChange: PropTypes.any
};

View File

@@ -49,7 +49,7 @@ export default function Tile({ data, children }) {
Cycle Count <ArrowRightIcon />
</Box>
</Link>
<Link to="/">
<Link to={`/setup/inventory/browse/${data.widgetname}/${data.id}`}>
<Box className={`${classes.box} ${classes.boxEven}`}>
{data.widgetname} List <ArrowRightIcon />
</Box>

View File

@@ -46,7 +46,7 @@ function MaterialForm({ formType, setFormOpen, selected, inventoryId }) {
: dispatch(
WidgetActions.editWidgetRequest({
loader: 'location-request',
slug: `${API.EDIT_WIDGET_FAMILY}${inventoryId}`,
slug: `${API.EDIT_WIDGET_FAMILY}${formType._id}`,
method: 'patch',
data: values,
type: 'edit'
@@ -113,6 +113,7 @@ function WidgetNestedDataTable({
inventoryId
}) {
const [open, setOpen] = React.useState(false);
const dispatch = useDispatch();
const widgetChildren = useSelector(WidgetSelectors.getWidgetsByParentId(data._id));
return (
@@ -167,46 +168,44 @@ function WidgetNestedDataTable({
</MDButton>
</Grid>
<Grid
container
item
xs={10}
xs={8}
onClick={() => {
setSelected(data);
}}
>
<Grid item xs={9}>
{data.name}
</Grid>
<Grid item xs={1}>
<MDButton
disabled
size="small"
variant="contained"
color="error"
sx={{
textTransform: 'capitalize',
minWidth: '45px',
minHeight: '28px',
marginLeft: '5px',
marginRight: '20px',
boxShadow: 'none',
fontWeight: '500',
padding: '0 6'
}}
onClick={() => {
// dispatch(
// WarehouseLocationsActions.deleteLocationRequest({
// loader: 'location-request',
// slug: API.LOCATION_DELETE,
// method: 'post',
// data: { type: data.location, id: data.id }
// })
// );
}}
>
DELETE
</MDButton>
</Grid>
{data.name}
</Grid>
<Grid item xs={2}>
<MDButton
size="small"
variant="contained"
color="error"
sx={{
textTransform: 'capitalize',
minWidth: '45px',
minHeight: '28px',
marginLeft: '5px',
marginRight: '20px',
boxShadow: 'none',
fontWeight: '500',
padding: '0 6'
}}
onClick={() => {
setSelected(null);
dispatch(
WidgetActions.editWidgetRequest({
loader: 'location-request',
slug: `${API.EDIT_WIDGET_FAMILY}${data._id}`,
deletedId: data._id,
method: 'delete',
type: 'delete'
})
);
}}
>
DELETE
</MDButton>
</Grid>
</Grid>
{open && widgetChildren ? (

View File

@@ -18,5 +18,6 @@ export default {
LOCATION_DELETE: '/dashboard/delete-location',
GET_WIDGET_FAMILY_BY_INVENTORY: '/widget-family/search-by-inventory?inventory=',
ADD_WIDGET_FAMILY: '/widget-family',
EDIT_WIDGET_FAMILY: '/widget-family/'
EDIT_WIDGET_FAMILY: '/widget-family/',
GET_ITEMS_BY_INVENTORY: '/item/filter?inventory='
};

View File

@@ -21,7 +21,6 @@ import MDButton from 'components/Button';
import { useFormik } from 'formik';
import schema from 'services/ValidationServices';
import MDInput from 'components/MDInput';
import { useLocation } from 'react-router-dom';
import WarehouseActions from 'redux/WarehouseRedux';
import SnackBar from 'components/SnackBar';
import { getChildLocationType } from 'utils/nestedTableTools';
@@ -36,6 +35,7 @@ import { WarehouseLocationsSelectors } from 'redux/WarehouseLocationsRedux';
import { API } from 'constant';
import NestedDataTable from 'components/NestedTable';
import Breadcrumbs from 'components/Breadcrumbs';
import { WarehouseSelectors } from 'redux/WarehouseRedux';
const bottomButtonStyling = {
width: '100%',
@@ -317,7 +317,9 @@ const WarehouseNestedDetails = () => {
const inventoryTypes = ['Perishable', 'Material', 'Product', 'Inventory', 'Fleet'];
function EditWarehouseDetails() {
const location = useLocation();
const { warehouseId } = useParams();
const warehouseData = useSelector(WarehouseSelectors.getWarehouseDetailById(warehouseId));
const [open, setOpen] = useState(false);
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
@@ -329,21 +331,22 @@ function EditWarehouseDetails() {
}
}
};
const dispatch = useDispatch();
const formik = useFormik({
initialValues: {
warehousename: location.state.name,
address: location.state.address,
warehousename: warehouseData.name,
address: warehouseData.address,
inventorytype: [],
attributes: '',
images: []
images: warehouseData.images
},
validationSchema: schema.warehouseForm,
onSubmit: (values, onSubmitProps) => {
dispatch(
WarehouseActions.editWarehouseAction({
loader: 'loading-request',
slug: `/warehouse/${location.state.id}`,
slug: `/warehouse/${warehouseData._id}`,
method: 'patch',
data: {
name: values.warehousename,
@@ -373,7 +376,7 @@ function EditWarehouseDetails() {
{ name: 'Home', path: '/home' },
{ name: 'Setup', path: '/setup' },
{ name: 'Warehouse', path: '/setup/warehouse' },
{ name: location.state.name || '' }
{ name: warehouseData.name || '' }
]}
/>
<Box mx={3} my={3}>

View File

@@ -103,7 +103,7 @@ function InventoryScreen() {
preferredLocations: false, // TODO: change later when implemented on BE
inventory_process: currentInventoryData.policies.inventory_process
},
image: [{ src: currentInventoryData.image }]
image: [{ src: currentInventoryData.image_url }]
}
: {
name: '',
@@ -118,7 +118,7 @@ function InventoryScreen() {
image: []
},
validationSchema: schema.addInventory,
onSubmit: (values, onSubmitProps) => {
onSubmit: (values) => {
LOGGER.log('values', values);
inventoryId
? dispatch(
@@ -144,9 +144,11 @@ function InventoryScreen() {
})
);
// navigate to edit inventory page
onSubmitProps.resetForm();
// onSubmitProps.resetForm();
}
});
LOGGER.log('Form values', formik.values);
return (
<DashboardLayout>
<DashboardNavbar />
@@ -178,7 +180,6 @@ function InventoryScreen() {
</Box>
<MDInput
fullWidth
disabled={inventoryId}
name="name"
type="text"
variant="outlined"
@@ -195,7 +196,6 @@ function InventoryScreen() {
</Box>
<MDInput
fullWidth
disabled={inventoryId}
name="widgetName"
type="text"
variant="outlined"
@@ -227,7 +227,6 @@ function InventoryScreen() {
<div sx={customStyles.gridWrap} key={item.key}>
<MDTypography sx={customStyles.textWrap}>{item.text}</MDTypography>
<Switch
disabled={inventoryId}
name={`policies.${item.key}`}
checked={formik.values.policies[item.key]}
onChange={formik.handleChange}
@@ -241,7 +240,6 @@ function InventoryScreen() {
<Select
select
fullWidth
disabled={inventoryId}
variant="outlined"
name="policies.inventory_process"
value={formik.values.policies.inventory_process}
@@ -279,23 +277,35 @@ function InventoryScreen() {
>
{'CANCEL'}
</MDButton>
<MDButton sx={{ ml: 3 }} color="primary" variant="outlined" type="submit">
{'SAVE'}
</MDButton>
<MDButton
sx={{ ml: 3 }}
color="primary"
variant="outlined"
disabled={inventoryId}
type="submit"
onClick={() => {
navigate(
`/setup/inventory/new-item/${currentInventoryData.widgetName}/${inventoryId}`
);
}}
>
{'SAVE'}
</MDButton>
<MDButton sx={{ ml: 3 }} color="primary">
{'ADD ITEMS'}
</MDButton>
</MDBox>
</Grid>
</MDBox>
</form>
{inventoryId ? <WidgetNestedDataTable inventoryId={inventoryId} /> : null}
{inventoryId ? (
<>
<MDBox sx={{ my: 4 }}>
<MDTypography variant="h5">Widget family hierarchy</MDTypography>
<MDTypography sx={customStyles.textSize}>
Define widget family and sub-family
</MDTypography>
</MDBox>
<WidgetNestedDataTable inventoryId={inventoryId} />
</>
) : null}
</MDBox>
</DashboardLayout>
);

View File

@@ -0,0 +1,68 @@
import React from 'react';
import MDBox from 'components/MDBox';
import DashboardNavbar from 'components/DashboardNavbar';
import DashboardLayout from 'layouts/DashboardLayout';
import Breadcrumbs from 'components/Breadcrumbs';
import { useDispatch, useSelector } from 'react-redux';
import ItemActions from 'redux/ItemRedux';
import { API } from 'constant';
import { useParams } from 'react-router-dom';
import LOGGER from 'services/Logger';
import { ItemSelectors } from 'redux/ItemRedux';
import EnhancedTable from 'components/EnhancedTable';
const tHeads = [
{ key: 'name', name: '' },
{ key: 'commonName', name: 'Common Name' },
{ key: 'formalName', name: 'Formal Name' },
{ key: 'description', name: 'Description' },
{ key: 'manufacturer', name: 'Manufacturer' }
];
function ItemListing() {
const dispatch = useDispatch();
const { widgetName, inventoryId } = useParams();
const [page /*, setPage*/] = React.useState(0);
const [perPage /*, setPerPage*/] = React.useState(10);
LOGGER.log({ widgetName, inventoryId });
const data = useSelector(ItemSelectors.getItemsByInventoryId(inventoryId));
React.useEffect(
() => {
dispatch(
ItemActions.itemRequest({
loader: 'loading-request',
slug: API.GET_ITEMS_BY_INVENTORY,
method: 'get',
page,
perPage,
inventoryId
})
);
},
[
/* page, perPage */
]
);
return (
<DashboardLayout>
<DashboardNavbar />
<Breadcrumbs
route={[
{ name: 'Home', path: '/home' },
{ name: 'Setup', path: '/setup' },
{ name: 'Inventory', path: '/setup/inventory' },
{ name: `${widgetName}s List` }
]}
/>
<MDBox px={2} py={3}>
List of {widgetName}s{/* <pre>{JSON.stringify(data, null, 4)}</pre> */}
<EnhancedTable data={data} tHeads={tHeads} />
</MDBox>
</DashboardLayout>
);
}
export default ItemListing;

View File

@@ -1,313 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import MDButton from 'components/Button';
import DashboardNavbar from 'components/DashboardNavbar';
import DashboardLayout from 'layouts/DashboardLayout';
import {
Box,
Chip,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
MenuItem,
OutlinedInput,
Select,
TextField
} from '@mui/material';
import NestedDataTable from 'components/NestedTable';
import { useDispatch, useSelector } from 'react-redux';
import WarehouseLocationsActions from 'redux/WarehouseLocationsRedux';
import { API } from 'constant';
import { WarehouseLocationsSelectors } from 'redux/WarehouseLocationsRedux';
import LOGGER from 'services/Logger';
import { getPropertiesOfLocationType } from 'utils/nestedTableTools';
import { useFormik } from 'formik';
import { getInitialvaluesFromParentData } from 'utils/nestedTableTools';
import { toTitleCase } from 'utils/nestedTableTools';
import { getChildLocationType } from 'utils/nestedTableTools';
import { getAPIslugOfLocationType } from 'utils/nestedTableTools';
import { useParams } from 'react-router-dom';
const bottomButtonStyling = {
width: '100%',
textTransform: 'uppercase',
borderRadius: '100px',
padding: '13px 30px'
};
const AddForm = ({ addFormOpen, setAddFormOpen, selected, warehouseId }) => {
const dispatch = useDispatch();
const data = addFormOpen !== 'zone' ? selected : { location: 'warehouse', id: warehouseId };
const childLocationType = getChildLocationType(data.location);
const fields = getPropertiesOfLocationType(childLocationType);
const formik = useFormik({
initialValues: getInitialvaluesFromParentData(data),
onSubmit: (values) => {
LOGGER.log('Form values and parent info', values, data);
const formData = { ...values };
formData[`${data.location}_id`] = data.id;
dispatch(
WarehouseLocationsActions.addLocationRequest({
loader: 'location-request',
slug: getAPIslugOfLocationType(childLocationType),
method: 'post',
data: formData,
parent: {
id: data.id,
type: data.location
}
})
);
setAddFormOpen(false);
}
});
return (
<Dialog
open={addFormOpen}
onClose={() => {
setAddFormOpen(false);
}}
>
<DialogTitle>Add new {childLocationType} details</DialogTitle>
<DialogContent>
{/* <DialogContentText>Some more text if needed</DialogContentText> */}
{fields &&
fields.map((fieldName) => (
<TextField
autoFocus
fullWidth
key={fieldName}
margin="dense"
label={toTitleCase(fieldName)}
type={fieldName === 'number' ? 'number' : 'text'}
name={fieldName}
variant="standard"
value={formik.values[fieldName]}
error={formik.touched[fieldName] && Boolean(formik.errors[fieldName])}
helperText={formik.touched[fieldName] && formik.errors[fieldName]}
onChange={formik.handleChange}
/>
))}
{childLocationType === 'sublevel' ? (
<>
Type:{' '}
<Select
label="Type"
name="type"
value={formik.values.type}
onChange={formik.handleChange}
>
<MenuItem value="POSITION">Position</MenuItem>
<MenuItem value="BIN">Bin</MenuItem>
<MenuItem value="PALLET">Pallet</MenuItem>
</Select>
Positions:{' '}
<Select
multiple
name="postitions"
value={formik.values.positions}
input={<OutlinedInput id="select-multiple-chip" label="Positions" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} />
))}
</Box>
)}
MenuProps={{
PaperProps: {
style: {
maxHeight: 48 * 4.5 + 8,
width: 250
}
}
}}
onChange={(event) => {
const {
target: { value }
} = event;
formik.setFieldValue(
'positions',
// On autofill we get a stringified value.
typeof value === 'string' ? value.split(',') : value
);
}}
>
{['LDB', 'LDF', 'LUB', 'LUF', 'RDB', 'RDF', 'RUB', 'RUF'].map((position) => (
<MenuItem
key={position}
value={position}
// style={{
// fontWeight: theme.typography.fontWeightMedium
// }}
>
{position}
</MenuItem>
))}
</Select>
</>
) : null}
</DialogContent>
<DialogActions>
<MDButton
onClick={() => {
setAddFormOpen(false);
}}
>
Cancel
</MDButton>
<MDButton onClick={formik.handleSubmit}>Save</MDButton>
</DialogActions>
</Dialog>
);
};
AddForm.propTypes = {
addFormOpen: PropTypes.any,
setAddFormOpen: PropTypes.any,
selected: PropTypes.any,
warehouseId: PropTypes.any
};
const WarehouseNestedDetails = () => {
const [selected, setSelected] = React.useState(null);
const [addFormOpen, setAddFormOpen] = React.useState(false);
const dispatch = useDispatch();
const { warehouseId } = useParams();
LOGGER.log('warehouseID', warehouseId);
const data = useSelector(WarehouseLocationsSelectors.getChildrenOfParent(warehouseId));
const populateChildren = (id, type) => {
LOGGER.log('populating:', id, type);
dispatch(
WarehouseLocationsActions.locationRequest({
loader: 'location-request',
slug: API.GET_CHILDREN_FROM_PARENT,
method: 'post',
data: { id, type }
})
);
};
React.useEffect(() => {
populateChildren(warehouseId, 'warehouse');
}, []);
return (
<>
<DashboardLayout>
<DashboardNavbar />
<Box px={3} py={3}>
{data &&
data.map((data) => (
<NestedDataTable
key={data.id}
data={data}
selected={selected}
setSelected={setSelected}
populateChildren={populateChildren}
/>
))}
{/* Debugging */}
{/* <pre>{JSON.stringify(selected, null, 4)}</pre> */}
{/* Bottom buttons */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
columnGap: '20px',
margin: '20px'
}}
>
<MDButton
size="medium"
sx={bottomButtonStyling}
color="primary"
variant="contained"
onClick={() => {
setAddFormOpen('zone');
}}
>
Add zone
</MDButton>
<MDButton
size="medium"
sx={bottomButtonStyling}
disabled={selected?.location !== 'zone'}
color={selected?.location === 'zone' ? 'primary' : 'secondary'}
variant="contained"
onClick={() => {
setAddFormOpen(true);
}}
>
Add area
</MDButton>
<MDButton
size="medium"
sx={bottomButtonStyling}
disabled={selected?.location !== 'area'}
color={selected?.location === 'area' ? 'primary' : 'secondary'}
variant="contained"
onClick={() => {
setAddFormOpen(true);
}}
>
Add row
</MDButton>
<MDButton
size="medium"
sx={bottomButtonStyling}
disabled={selected?.location !== 'row'}
color={selected?.location === 'row' ? 'primary' : 'secondary'}
variant="contained"
onClick={() => {
setAddFormOpen(true);
}}
>
Add bay
</MDButton>
<MDButton
size="medium"
sx={bottomButtonStyling}
disabled={selected?.location !== 'bay'}
color={selected?.location === 'bay' ? 'primary' : 'secondary'}
variant="contained"
onClick={() => {
setAddFormOpen(true);
}}
>
Add Level
</MDButton>
<MDButton
size="medium"
sx={bottomButtonStyling}
disabled={!['level', 'sublevel'].includes(selected?.location)}
color={['level', 'sublevel'].includes(selected?.location) ? 'primary' : 'secondary'}
variant="contained"
onClick={() => {
setAddFormOpen(true);
}}
>
Add Sublevel
</MDButton>
</Box>
</Box>
{addFormOpen && (
<AddForm
addFormOpen={addFormOpen}
setAddFormOpen={setAddFormOpen}
selected={selected}
warehouseId={warehouseId}
/>
)}
</DashboardLayout>
</>
);
};
export default WarehouseNestedDetails;

View File

@@ -93,8 +93,8 @@ export const onUpdateInventorySuccess = (state, { data }) =>
fetching: getFetchingValue(state.fetching, data?.loader),
error: getErrorValue(state?.error, data?.loader),
getInventoryDetail: [
...state.getInventoryDetail.filter((x) => x._id !== data.newInventory._id),
data.newInventory
...state.getInventoryDetail.filter((x) => x._id !== data.updateInventory?.inventory?._id),
data.updateInventory?.inventory
]
});

100
src/redux/ItemRedux.js Normal file
View File

@@ -0,0 +1,100 @@
import { createActions, createReducer } from 'reduxsauce';
import Immutable from 'seamless-immutable';
import _ from 'underscore';
import { getFetchingValue, getErrorValue } from '../services/Utils';
/* ------------- Types and Action Creators ------------- */
const { Types, Creators } = createActions({
itemRequest: ['payload'],
itemSuccess: ['data'],
editItemRequest: ['payload'],
editItemSuccess: ['data'],
itemFailure: ['error'],
logout: null
});
export const ItemTypes = Types;
const ItemActions = Creators;
export default ItemActions;
/* ------------- Initial State ------------- */
export const INITIAL_STATE = Immutable({
list: [],
fetching: [],
error: {}
});
/* ------------- Selectors ------------- */
export const ItemSelectors = {
getItems: (state) => state.items.list,
getItemById: (id) => (state) => state.items.list.find((x) => x._id === id),
getItemsByInventoryId: (id) => (state) =>
state.items.list.filter((x) => x.widgetFamily?.inventory === id)
};
/* ------------- Reducers ------------- */
export const onItemRequest = (state, { payload }) =>
state.merge({
fetching: _.uniq([...state.fetching, payload?.loader]),
error: getErrorValue(state?.error, payload?.loader)
});
export const onEditItemRequest = (state, { payload }) =>
state.merge({
fetching: _.uniq([...state.fetching, payload?.loader]),
error: getErrorValue(state?.error, payload?.loader)
});
const mergeItemStates = (stateData, items) => {
if (!items) return stateData; // undefined check
const idsInNewItems = items.map((x) => x._id);
const newState = stateData.filter((x) => !idsInNewItems.includes(x._id));
return [...newState, ...items];
};
export const onItemSuccess = (state, { data }) =>
state.merge({
fetching: getFetchingValue(state.fetching, data?.loader),
error: getErrorValue(state?.error, data?.loader),
list: mergeItemStates(state.list, data.items)
});
const mergeEditItemStates = (stateList, item, type) => {
if (!item) return stateList; // undefined check
if (type === 'add') {
return [...stateList, item];
} else if (type === 'edit') {
const newState = stateList.filter((x) => x._id !== item._id);
return [...newState, item];
// } else if (type === 'delete') {
// return stateList.filter((x) => x._id !== item._id);
} else {
return stateList;
}
};
export const onEditItemSuccess = (state, { data }) =>
state.merge({
fetching: getFetchingValue(state.fetching, data?.loader),
error: getErrorValue(state?.error, data?.loader),
list: mergeEditItemStates(state.list, data.item, data.type)
});
export const onItemFailure = (state, { error }) =>
state.merge({
fetching: _.without(state.fetching, error?.loader),
error: { ...state.error, [error?.loader]: error?.error }
});
/* ------------- Hookup Reducers To Types ------------- */
export const itemReducer = createReducer(INITIAL_STATE, {
[Types.ITEM_REQUEST]: onItemRequest,
[Types.EDIT_ITEM_REQUEST]: onEditItemRequest,
[Types.ITEM_SUCCESS]: onItemSuccess,
[Types.EDIT_ITEM_SUCCESS]: onEditItemSuccess,
[Types.ITEM_FAILURE]: onItemFailure
});

View File

@@ -39,6 +39,8 @@ export const INITIAL_STATE = Immutable({
/* ------------- Selectors ------------- */
export const WarehouseSelectors = {
getWarehouseDetail: (state) => state.warehouse.warehouseDetail,
getWarehouseDetailById: (id) => (state) =>
state.warehouse.warehouseDetail.find((x) => x._id === id),
createWarehouseDetail: (state) => state.warehouse.createWarehouse,
editWarehouseDetail: (state) => state.warehouse.editWarehouse
};

View File

@@ -65,14 +65,16 @@ export const onWidgetSuccess = (state, { data }) =>
list: mergeWidgetStates(state.list, data.widgets)
});
const mergeEditWidgetStates = (stateList, widget, type) => {
if (!widget) return stateList; // undefined check
const mergeEditWidgetStates = (stateList, widget, type, deletedId) => {
if (!(widget || deletedId)) return stateList; // undefined check
if (type === 'add') {
return [...stateList, widget];
} else if (type === 'edit') {
const newState = stateList.filter((x) => x._id !== widget._id);
return [...newState, widget];
} else if (type === 'delete') {
return stateList.filter((x) => x._id !== deletedId);
} else {
return stateList;
}
@@ -82,7 +84,7 @@ export const onEditWidgetSuccess = (state, { data }) =>
state.merge({
fetching: getFetchingValue(state.fetching, data?.loader),
error: getErrorValue(state?.error, data?.loader),
list: mergeEditWidgetStates(state.list, data.widget, data.type)
list: mergeEditWidgetStates(state.list, data.widget, data.type, data.deletedId)
});
export const onWidgetFailure = (state, { error }) =>

View File

@@ -7,6 +7,7 @@ import { inventoryReducer } from './InventoryRedux';
import { rolesReducer } from './RolesRedux';
import { WarehouseLocationsReducer } from './WarehouseLocationsRedux';
import { widgetReducer } from './WidgetRedux';
import { itemReducer } from './ItemRedux';
// Combine all reducers.
const appReducer = combineReducers({
@@ -17,7 +18,8 @@ const appReducer = combineReducers({
warehouseLocations: WarehouseLocationsReducer,
product: productReducer,
inventory: inventoryReducer,
widgets: widgetReducer
widgets: widgetReducer,
items: itemReducer
});
const rootReducer = (state, action) => {

View File

@@ -51,7 +51,6 @@ import LocationLabelingScreen from 'pages/labeling';
import UserAccessScreen from 'pages/useraccess';
import NewWarehouseDetails from 'pages/newWarehouseDetails';
import SetupHome from 'pages/setup';
import WarehouseDetailsTables from 'pages/warehouseDetailsTables';
import EditWarehouseDetails from 'pages/editWarehouseDetails';
import LabelingHome from 'pages/labellingHome';
import SetupInventory from 'pages/setupInventory';
@@ -60,6 +59,7 @@ import SetupIcon from 'assets/images/SetupIcon';
import AddNewItem from '../pages/addNewProduct';
import CreateUserRole from 'pages/createUserRole';
import WidgetLabel from 'pages/widgetLabel';
import ItemListing from 'pages/itemListing';
// Images
// import profilePicture from 'assets/images/team-3.jpg';
@@ -158,6 +158,13 @@ const protectedRoutes = [
route: '/setup/inventory/new-item/:widgetName/:inventoryId',
component: <AddNewItem />
},
{
name: 'View Items',
key: 'view-items',
hide: true,
route: '/setup/inventory/browse/:widgetName/:inventoryId',
component: <ItemListing />
},
{
name: 'Location Labeling',
key: 'location-labeling',
@@ -177,13 +184,6 @@ const protectedRoutes = [
route: '/setup/labeling',
component: <LabelingHome />
},
{
name: 'Warehouse Details',
key: 'warehouse-details',
hide: true,
route: '/setup/warehouse/warehouse-details/:warehouseId',
component: <WarehouseDetailsTables />
},
{
name: 'Add Warehouse',
key: 'add-warehouse',

View File

@@ -1,5 +1,6 @@
import { AuthorizedAPI } from 'config';
import { takeLatest, call, put, takeEvery } from 'redux-saga/effects';
import { toast } from 'react-toastify';
import { call, put, takeEvery } from 'redux-saga/effects';
import InventoryActions from 'redux/InventoryRedux';
import { InventoryTypes } from 'redux/InventoryRedux';
import ApiServices from 'services/API/ApiServices';
@@ -101,10 +102,11 @@ export function* onRequestUpdateInventoryData({ payload }) {
payload?.data
);
if (response?.status === 200) {
toast('Updated inventory successfully');
yield put(
InventoryActions.updateInventorySuccess({
loader: payload?.loader,
updateInventoryDetail: response?.data?.data
updateInventory: response?.data?.data
})
);
} else {
@@ -118,8 +120,8 @@ export function* onRequestUpdateInventoryData({ payload }) {
}
}
export default [
takeLatest(InventoryTypes.GET_INVENTORY_ACTION, onRequestGetInventoryData),
takeLatest(InventoryTypes.ADD_INVENTORY_ACTION, onRequestAddInventoryData),
takeLatest(InventoryTypes.UPDATE_INVENTORY_ACTION, onRequestUpdateInventoryData),
takeEvery(InventoryTypes.GET_INVENTORY_ACTION, onRequestGetInventoryData),
takeEvery(InventoryTypes.ADD_INVENTORY_ACTION, onRequestAddInventoryData),
takeEvery(InventoryTypes.UPDATE_INVENTORY_ACTION, onRequestUpdateInventoryData),
takeEvery(InventoryTypes.GET_INVENTORY_TYPES_ACTION, onRequestGetInventoryTypesData)
];

60
src/sagas/Item.js Normal file
View File

@@ -0,0 +1,60 @@
import { AuthorizedAPI } from 'config';
import { call, put, takeEvery } from 'redux-saga/effects';
import ApiServices from 'services/API/ApiServices';
import ItemActions, { ItemTypes } from '../redux/ItemRedux';
export function* onRequestItem({ payload }) {
const response = yield call(
ApiServices[payload?.method],
AuthorizedAPI,
`${payload?.slug}${payload?.inventoryId}&page=${payload?.page}&perPage=${payload?.perPage}`,
payload?.data
);
if (response?.status === 200) {
yield put(
ItemActions.itemSuccess({
loader: payload?.loader,
items: response?.data?.data,
page: payload?.page,
reset: !payload.page
})
);
} else {
yield put(
ItemActions.itemFailure({
loader: payload?.loader,
error: response?.message
})
);
}
}
export function* onEditRequestItem({ payload }) {
const response = yield call(
ApiServices[payload?.method],
AuthorizedAPI,
payload?.slug,
payload?.data
);
if (response?.status === 200) {
yield put(
ItemActions.editItemSuccess({
loader: payload?.loader,
item: response?.data?.data,
type: payload?.type
})
);
} else {
yield put(
ItemActions.itemFailure({
loader: payload?.loader,
error: response?.message
})
);
}
}
export default [
takeEvery(ItemTypes.ITEM_REQUEST, onRequestItem),
takeEvery(ItemTypes.EDIT_ITEM_REQUEST, onEditRequestItem)
];

View File

@@ -39,7 +39,8 @@ export function* onEditRequestWidget({ payload }) {
WidgetActions.editWidgetSuccess({
loader: payload?.loader,
widget: response?.data?.data,
type: payload?.type
type: payload?.type,
deletedId: payload?.deletedId
})
);
} else {

View File

@@ -7,6 +7,7 @@ import InventorySaga from './Inventory';
import RolesSaga from './Roles';
import WarehouseLocationsSaga from './WarehouseLocations';
import WidgetSaga from './Widget';
import ItemSaga from './Item';
export default function* rootSaga() {
yield all([...AuthSaga]);
@@ -17,4 +18,5 @@ export default function* rootSaga() {
yield all([...RolesSaga]);
yield all([...WarehouseLocationsSaga]);
yield all([...WidgetSaga]);
yield all([...ItemSaga]);
}