Feature/wms 39 nestedtable (#62)

* Updated: routes
* Rewritten nested table logic
* Updated: page with sample data and removed unnecessary components
* Removed: footer
* Fix: initial selected null handler
* Added: styling
* Updated: Table/button styling
* Added: API responses
* Fix: dispatch parameters
* Update: disable dropdown when no children
* Update: better receiving of data
* Update: better click handling, row distribution
* Update: yay the add form works finally
* Added: navigation
* Fixed: add zone button
* Added some colours
* Added: positions for sublevel
* Fixed: null check
* Added: initial sublevel values
* Update: fixed add sublevel
* Removed: location edit in sublevel
* Fixed: sublevel locations
* Added: edit apis handler
* Added: dispatch
* Fix: handle type seperately
* Added: edit warehouse redux
* Removed: loggers
* Update: edit form payload method
* Fix: multiple requests

Co-authored-by: Llewellyn D'souza <lledsouza2209@gmail.com>
This commit is contained in:
bluestreamlds
2022-02-25 11:08:55 +05:30
committed by GitHub
parent b606cecb24
commit 2ce13ba312
10 changed files with 948 additions and 316 deletions

View File

@@ -1,287 +1,267 @@
import * as React from 'react';
import Box from '@mui/material/Box';
/* eslint-disable indent */
import {
Box,
// Chip,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Grid,
MenuItem,
// OutlinedInput,
Select,
TextField
} from '@mui/material';
import React, { useEffect } from 'react';
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 KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import IconButton from '@mui/material/IconButton';
import Collapse from '@mui/material/Collapse';
import TablePagination from 'components/TablePagination';
import MDButton from 'components/Button';
import { useDispatch, useSelector } from 'react-redux';
import { WarehouseLocationsSelectors } from 'redux/WarehouseLocationsRedux';
import LOGGER from 'services/Logger';
import { useFormik } from 'formik';
import { getPropertiesOfLocationType } from 'utils/nestedTableTools';
import { getInitialvaluesFromData } from 'utils/nestedTableTools';
import { getColorOfLocationType } from 'utils/nestedTableTools';
import { toTitleCase } from 'utils/nestedTableTools';
import WarehouseLocationsActions from 'redux/WarehouseLocationsRedux';
import { getAPIslugOfLocationType } from 'utils/nestedTableTools';
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(4n+1)': {
backgroundColor: theme.palette.action.hover
},
'td, th' :{
padding: '0.75rem 0.5rem'
},
// hide last border
'&:last-child td, &:last-child th': {
border: 0
}
// '&:nth-of-type(1) td, &:nth-of-type(1) th': {
// width: '10px'
// }
}));
function createData(zone, names, type, specifications) {
return {
zone,
names,
type,
specifications,
children:[
{
rownumber:'10,654',
location:'ALFKI',
employeeid:'4',
orderdate:'08/25/2008',
requiredate:'09/22/2008',
shippeddate:'09/22/2008'
},
{
rownumber:'10,654',
location:'ALFKI',
employeeid:'4',
orderdate:'08/25/2008',
requiredate:'09/22/2008',
shippeddate:'09/22/2008'
}
]
};
}
const rows = [
createData('Zone A', 'Semper libero sit element...', 'Ana Trujillo', 'Orci arcu dictum pellentesque'),
createData('Zone A', 'Semper libero sit element...', 'Ana Trujillo', 'Orci arcu dictum pellentesque'),
createData('Zone A', 'Semper libero sit element...', 'Ana Trujillo', 'Orci arcu dictum pellentesque'),
createData('Zone A', 'Semper libero sit element...', 'Ana Trujillo', 'Orci arcu dictum pellentesque'),
createData('Zone A', 'Semper libero sit element...', 'Ana Trujillo', 'Orci arcu dictum pellentesque')
];
Row.propTypes = {
row: PropTypes.shape({
zone: PropTypes.string.isRequired,
names: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
specifications: PropTypes.string.isRequired,
children: PropTypes.arrayOf(
PropTypes.shape({
rownumber: PropTypes.string.isRequired,
location: PropTypes.string.isRequired,
employeeid: PropTypes.string.isRequired,
orderdate: PropTypes.string.isRequired,
requiredate: PropTypes.string.isRequired,
shippeddate: PropTypes.string.isRequired
}),
).isRequired
}).isRequired
};
function Row(props) {
const { row } = props;
function NestedDataTable({ data, selected, setSelected, populateChildren }) {
const dispatch = useDispatch();
const [open, setOpen] = React.useState(false);
const [editFormOpen, setEditFormOpen] = React.useState(false);
const children = useSelector(WarehouseLocationsSelectors.getChildrenOfParent(data.id));
const fields = getPropertiesOfLocationType(data.location);
const formik = useFormik({
initialValues: getInitialvaluesFromData(data),
onSubmit: (values /*, { resetForm, setSubmitting }*/) => {
LOGGER.log('Form values and field info', values, data);
const formData = { ...values };
// formData[`${data.location}_id`] = data.id;
dispatch(
WarehouseLocationsActions.editLocationRequest({
loader: 'location-request',
slug: getAPIslugOfLocationType(data.location) + `/${data.id}`,
method: 'patch',
data: formData,
child: {
id: data.id,
parentId: data.parentId,
type: data.location
}
})
);
setEditFormOpen(false);
}
});
return (
<React.Fragment>
<StyledTableRow sx={{ '&odd > *': { borderBottom: 'unset' } }}>
<StyledTableCell sx={{ width:'10%', display:'flex', alignItems: 'center' }}>
<IconButton
aria-label="expand row"
size="small"
sx ={{ border: '1px solid #ccc' }}
onClick={() => setOpen(!open)}
>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
<MDButton size="small" variant="contained" color="primary" sx={{ textTransform:'capitalize', minWidth:'45px', minHeight:'28px', marginLeft:'10px', boxShadow:'none', fontWeight:'500', padding:'0' }}>EDIT</MDButton>
useEffect(() => {
populateChildren(data.id, data.location);
}, []);
</StyledTableCell>
<StyledTableCell component="th" scope="row" sx={{ textTransform:'uppercase', fontWeight:'400', fontSize:'14px', width:'5%' }}>
{row.zone}
</StyledTableCell>
<StyledTableCell>{row.names}</StyledTableCell>
<StyledTableCell>{row.type}</StyledTableCell>
<StyledTableCell>{row.specifications}</StyledTableCell>
</StyledTableRow>
<TableRow>
<TableCell colSpan={5} style={{ paddingBottom: 0, paddingTop: 0 }} >
<Collapse unmountOnExit in={open}>
<Box sx={{ margin:'15px 104px' }}>
<Table size="small" aria-label="purchases" sx={{ border: '1.2px solid #C2C2C2' }}>
<TableHead sx={{ background: '#E5E7EB', display:'table-header-group !important' }}>
<TableRow>
<TableCell sx={{ fontWeight:'500', fontSize:'14px' }}>Row no</TableCell>
<TableCell sx={{ fontWeight:'500', fontSize:'14px' }}>Number of Bays</TableCell>
<TableCell sx={{ fontWeight:'500', fontSize:'14px' }}>Employee ID</TableCell>
<TableCell sx={{ fontWeight:'500', fontSize:'14px' }}>Order Date</TableCell>
<TableCell sx={{ fontWeight:'500', fontSize:'14px' }}>Required Date</TableCell>
<TableCell sx={{ fontWeight:'500', fontSize:'14px' }}>Shipped Date</TableCell>
</TableRow>
</TableHead>
<TableBody>
{row?.children?.map((item , i)=>{
return(
<TableRow key={i}>
<TableCell>{item.rownumber}</TableCell>
<TableCell>{item.location}</TableCell>
<TableCell>{item.employeeid}</TableCell>
<TableCell>{item.orderdate}</TableCell>
<TableCell>{item.requiredate}</TableCell>
<TableCell>{item.shippeddate}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</Box>
</Collapse>
</TableCell>
</TableRow>
</React.Fragment>
);
}
export default function NestedTable() {
const [anchorEl, setAnchorEl] = React.useState(null);
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
sx={{
borderLeftWidth: '2px',
borderLeftStyle: 'solid',
borderLeftColor: getColorOfLocationType(data.location),
borderTopWidth: '1px',
borderTopStyle: 'solid',
borderTopColor: '#BDD0DB',
background:
selected?.id === data.id
? 'linear-gradient(135deg, ' +
getColorOfLocationType(data.location) +
' 0%, #f9f9f9 20%)'
: '#f9f9f9'
}}
>
<Grid container key={data.id}>
<Grid item xs={3}>
<IconButton
disabled={!children?.length}
aria-label="expand row"
size="small"
sx={{ marginLeft: '12px' }}
onClick={() => setOpen(!open)}
>
{open ? <KeyboardArrowUpIcon /> : <KeyboardArrowRightIcon />}
</IconButton>
<MDButton
size="small"
variant="contained"
color="primary"
sx={{
textTransform: 'capitalize',
minWidth: '45px',
minHeight: '28px',
marginLeft: '5px',
marginRight: '20px',
boxShadow: 'none',
fontWeight: '500',
padding: '0'
}}
onClick={() => {
setEditFormOpen(true);
}}
>
EDIT
</MDButton>
{data.location}
</Grid>
<Grid
container
xs
onClick={() => {
setSelected(data);
}}
>
<Grid item xs={3}>
{data.name}
</Grid>
<Grid item xs={2}>
{['row', 'level', 'bay'].includes(data.location) ? data.number : data.type}
</Grid>
{data.location === 'bay' ? (
<Grid item xs={2}>
{data.type}
</Grid>
) : null}
<Grid item xs>
{data.specs}
</Grid>
</Grid>
</Grid>
{open && children ? (
<Box Box sx={{ marginLeft: '25px', marginBottom: '15px' }}>
{/* Add headers here */}
{children.map((data) => (
<NestedDataTable
key={data.id}
data={data}
selected={selected}
setSelected={setSelected}
populateChildren={populateChildren}
/>
))}
</Box>
) : null}
</Box>
{editFormOpen && (
<Dialog
open={editFormOpen}
onClose={() => {
setEditFormOpen(false);
}}
>
<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={handleClick}
>
Dashboard
</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>
{/* 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>
<StyledTableCell></StyledTableCell>
<StyledTableCell>Zone</StyledTableCell>
<StyledTableCell>Names</StyledTableCell>
<StyledTableCell>Type</StyledTableCell>
<StyledTableCell>Specifications</StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map((row) => (
<Row key={row.name} row={row} />
<DialogTitle>Edit {data.location} 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}
/>
))}
</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>
{data.location === '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="positions"
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={() => {
setEditFormOpen(false);
}}
>
Cancel
</MDButton>
<MDButton onClick={formik.handleSubmit}>Save</MDButton>
</DialogActions>
</Dialog>
)}
</>
);
}
NestedDataTable.propTypes = {
data: PropTypes.any,
selected: PropTypes.any,
setSelected: PropTypes.any,
populateChildren: PropTypes.any
};
export default NestedDataTable;

View File

@@ -3,7 +3,14 @@ export default {
GET_WAREHOUSE_DATA: '/warehouse/all?page=0&perPage=50',
CREATE_WAREHOUSE: '/warehouse/',
GET_USERS_DATA: '/user/all?page=0&perPage=10',
GET_ROLES_DATA: '/user-role/all?page=0&perPage=10',
GET_CHILDREN_FROM_PARENT: '/dashboard/get-children-from-parent',
ADD_NEW_ZONE: '/zone',
ADD_NEW_AREA: '/area',
ADD_NEW_ROW: '/row',
ADD_NEW_BAY: '/bay',
ADD_NEW_LEVEL: '/level',
ADD_NEW_SUBLEVEL: '/sublevel',
ADD_PRODUCT: '/item/',
ADD_INVENTORY: '/inventory',
GET_ROLES_DATA: '/user-role/all?page=0&perPage=10'
ADD_INVENTORY: '/inventory'
};

View File

@@ -12,6 +12,7 @@ import MDInput from 'components/MDInput';
import { useLocation } from 'react-router-dom';
import WarehouseActions from 'redux/WarehouseRedux';
import SnackBar from 'components/SnackBar';
import { useNavigate } from 'react-router-dom';
const useStyles = makeStyles({
labelSize: {
@@ -25,6 +26,7 @@ const useStyles = makeStyles({
const inventoryTypes = ['Perishable', 'Material', 'Product', 'Inventory', 'Fleet'];
function EditWarehouseDetails() {
const navigate = useNavigate();
const classes = useStyles();
const location = useLocation();
const [open, setOpen] = useState(false);
@@ -208,7 +210,14 @@ function EditWarehouseDetails() {
<MDButton size="large" color="primary" variant="outlined" type="submit">
EDIT DETAILS
</MDButton>
<MDButton size="large" color="primary" variant="contained">
<MDButton
size="large"
color="primary"
variant="contained"
onClick={() => {
navigate(`/setup/warehouse/warehouse-details/${location.state.id}`);
}}
>
SHOW DETAILS
</MDButton>
</Box>

View File

@@ -1,92 +1,313 @@
import React from 'react';
import PropTypes from 'prop-types';
import MDButton from 'components/Button';
import DashboardNavbar from 'components/DashboardNavbar';
import NestedTable from 'components/NestedTable';
import DashboardLayout from 'layouts/DashboardLayout';
import { makeStyles } from '@mui/styles';
import { Box } from '@mui/material';
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 useStyles = makeStyles({
customButton: {
width: '100%',
padding: '13px 30px !important',
textTransform: 'uppercase',
borderRadius: '100px !important',
boxShadow: 'none !important'
}
});
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', parentId: 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');
}, []);
const WarehouseDetailsTables = () => {
const classes = useStyles();
return (
<>
<DashboardLayout>
<DashboardNavbar />
<Box px={3} py={3}>
<NestedTable />
{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 0px'
margin: '20px'
}}
>
<MDButton
size="medium"
className={classes.customButton}
sx={bottomButtonStyling}
color="primary"
variant="contained"
onClick={() => {
setAddFormOpen('zone');
}}
>
add zone
Add zone
</MDButton>
<MDButton
size="medium"
className={classes.customButton}
color="primary"
sx={bottomButtonStyling}
disabled={selected?.location !== 'zone'}
color={selected?.location === 'zone' ? 'primary' : 'secondary'}
variant="contained"
onClick={() => {
setAddFormOpen(true);
}}
>
add area
Add area
</MDButton>
<MDButton
size="medium"
className={classes.customButton}
color="primary"
sx={bottomButtonStyling}
disabled={selected?.location !== 'area'}
color={selected?.location === 'area' ? 'primary' : 'secondary'}
variant="contained"
onClick={() => {
setAddFormOpen(true);
}}
>
add row
Add row
</MDButton>
<MDButton
size="medium"
className={classes.customButton}
color="primary"
sx={bottomButtonStyling}
disabled={selected?.location !== 'row'}
color={selected?.location === 'row' ? 'primary' : 'secondary'}
variant="contained"
sx={{ background: '#E5E7EB !important', color: 'rgba(75, 85, 99, 0.5)' }}
onClick={() => {
setAddFormOpen(true);
}}
>
add bay
Add bay
</MDButton>
<MDButton
size="medium"
className={classes.customButton}
color="primary"
sx={bottomButtonStyling}
disabled={selected?.location !== 'bay'}
color={selected?.location === 'bay' ? 'primary' : 'secondary'}
variant="contained"
sx={{ background: '#E5E7EB !important', color: 'rgba(75, 85, 99, 0.5)' }}
onClick={() => {
setAddFormOpen(true);
}}
>
split bay
Add Level
</MDButton>
<MDButton
size="medium"
className={classes.customButton}
color="primary"
sx={bottomButtonStyling}
disabled={!['level', 'sublevel'].includes(selected?.location)}
color={['level', 'sublevel'].includes(selected?.location) ? 'primary' : 'secondary'}
variant="contained"
sx={{ background: '#E5E7EB !important', color: 'rgba(75, 85, 99, 0.5)' }}
onClick={() => {
setAddFormOpen(true);
}}
>
split level
Add Sublevel
</MDButton>
</Box>
</Box>
{addFormOpen && (
<AddForm
addFormOpen={addFormOpen}
setAddFormOpen={setAddFormOpen}
selected={selected}
warehouseId={warehouseId}
/>
)}
</DashboardLayout>
</>
);
};
export default WarehouseDetailsTables;
export default WarehouseNestedDetails;

View File

@@ -0,0 +1,181 @@
import { createActions, createReducer } from 'reduxsauce';
import Immutable from 'seamless-immutable';
import LOGGER from 'services/Logger';
import _ from 'underscore';
import { getChildLocationType } from 'utils/nestedTableTools';
import { getFetchingValue, getErrorValue } from '../services/Utils';
/* ------------- Types and Action Creators ------------- */
const { Types, Creators } = createActions({
locationRequest: ['payload'],
locationSuccess: ['data'],
locationFailure: ['error'],
addLocationRequest: ['payload'],
editLocationRequest: ['payload']
});
export const WarehouseLocationsTypes = Types;
const WarehouseLocationsActions = Creators;
export default WarehouseLocationsActions;
/* ------------- Initial State ------------- */
export const INITIAL_STATE = Immutable({
childData: [],
fetching: [],
error: {}
});
/* ------------- Selectors ------------- */
export const WarehouseLocationsSelectors = {
getChildData: (state) => state.warehouseLocations.childData,
getChildrenOfParent: (id) => (state) =>
state.warehouseLocations.childData?.filter((c) => c.parentId === id),
// getChildrenOfParent: (id) => () => sampleState?.filter((c) => c.parentId === id),
fetching: (state) => state.warehouseLocations.fetching
};
/* ------------- Reducers ------------- */
export const onLocationRequest = (state, { payload }) =>
state.merge({
fetching: _.uniq([...state.fetching, payload?.loader]),
error: getErrorValue(state?.error, payload?.loader)
});
const mapResponseToNestedTable = (stateData, childData) => {
if (!childData) return stateData; // undefined check
let newChildren;
// incase edited
if (childData.edited) {
newChildren = childData?.childrenData.map((child) => ({
...child,
id: child._id,
location: childData.child.type,
parentId: childData.child.parentId
}));
} else {
// created
newChildren = childData?.childrenData.map((child) => ({
...child,
id: child._id,
location: getChildLocationType(childData.parent.type),
parentId: childData.parent.id
}));
}
const idsInNewChildren = newChildren.map((st) => st.id);
const newState = stateData.filter((st) => !idsInNewChildren.includes(st.id));
return [...newState, ...newChildren];
};
export const onLocationSuccess = (state, { data }) => {
LOGGER.log('From onLocationSuccess', state, data);
return state.merge({
fetching: getFetchingValue(state.fetching, data?.loader),
error: getErrorValue(state?.error, data?.loader),
childData: mapResponseToNestedTable(state.childData, data?.childData)
});
};
export const onLocationFailure = (state, { error }) =>
state.merge({
fetching: _.without(state.fetching, error?.loader),
error: { ...state.error, [error?.loader]: error?.error }
});
export const onAddLocationRequest = (state, { payload }) =>
state.merge({
fetching: _.uniq([...state.fetching, payload?.loader]),
error: getErrorValue(state?.error, payload?.loader)
});
export const onEditLocationRequest = (state, { payload }) =>
state.merge({
fetching: _.uniq([...state.fetching, payload?.loader]),
error: getErrorValue(state?.error, payload?.loader)
});
/* ------------- Hookup Reducers To Types ------------- */
export const WarehouseLocationsReducer = createReducer(INITIAL_STATE, {
[Types.LOCATION_REQUEST]: onLocationRequest,
[Types.LOCATION_SUCCESS]: onLocationSuccess,
[Types.LOCATION_FAILURE]: onLocationFailure,
[Types.ADD_LOCATION_REQUEST]: onAddLocationRequest,
[Types.EDIT_LOCATION_REQUEST]: onEditLocationRequest
});
// const sampleState = [
// {
// parentId: '61cea720ccc4b530015164f0',
// id: 132,
// location: 'Zone',
// name: 'Zone 1',
// type: 'Type 1',
// specifications: 'something really long, idk'
// },
// {
// parentId: 132,
// id: 154,
// location: 'Area',
// name: 'Area 1',
// type: 'Type 1',
// specifications: 'something really long, idk'
// },
// {
// parentId: 154,
// id: 254,
// location: 'Row',
// name: 'Row 2',
// type: 'Type 2',
// specifications: 'something really long, idk'
// },
// {
// parentId: 154,
// id: 233,
// location: 'Row',
// name: 'Row 2',
// type: 'Type 2',
// specifications: 'something really long, idk'
// },
// {
// parentId: 233,
// id: 254,
// location: 'Bay',
// name: 'Bay 2',
// type: 'Type 2',
// specifications: 'something really long, idk'
// },
// {
// parentId: 233,
// id: 954,
// location: 'Bay',
// name: 'Bay 2',
// type: 'Type 2',
// specifications: 'something really long, idk'
// },
// {
// parentId: 954,
// id: 4687,
// location: 'Level',
// name: 'Level 2',
// type: 'Type 2',
// specifications: 'something really long, idk'
// },
// {
// parentId: 954,
// id: 1264,
// location: 'Level',
// name: 'Level 2',
// type: 'Type 2',
// specifications: 'something really long, idk'
// },
// {
// parentId: 132,
// id: 133,
// location: 'Area',
// name: 'Area 1',
// type: 'Type 1',
// specifications: 'something really long, idk'
// }
// ];

View File

@@ -5,15 +5,17 @@ import { usersReducer } from './UsersRedux';
import { productReducer } from './ProductsRedux';
import { inventoryReducer } from './InventoryRedux';
import { rolesReducer } from './RolesRedux';
import { WarehouseLocationsReducer } from './WarehouseLocationsRedux';
// Combine all reducers.
const appReducer = combineReducers({
auth: authReducer,
warehouse: warehouseReducer,
users: usersReducer,
roles: rolesReducer,
warehouseLocations: WarehouseLocationsReducer,
product: productReducer,
inventory: inventoryReducer,
roles: rolesReducer
inventory: inventoryReducer
});
const rootReducer = (state, action) => {

View File

@@ -177,10 +177,10 @@ const protectedRoutes = [
component: <LabelingHome />
},
{
name: 'Warehouse Details Table',
key: 'warehouse-details-table',
name: 'Warehouse Details',
key: 'warehouse-details',
hide: true,
route: '/setup/warehouse/warehouse-details-table',
route: '/setup/warehouse/warehouse-details/:warehouseId',
component: <WarehouseDetailsTables />
},
{

View File

@@ -0,0 +1,99 @@
import { AuthorizedAPI } from 'config';
import { call, put, takeEvery } from 'redux-saga/effects';
import WarehouseLocationsActions from 'redux/WarehouseLocationsRedux';
import { WarehouseLocationsTypes } from 'redux/WarehouseLocationsRedux';
import ApiServices from 'services/API/ApiServices';
import LOGGER from 'services/Logger';
// import {
// getError,
// } from '../services/Utils';
export function* onRequestLocation({ payload }) {
const response = yield call(
ApiServices[payload?.method],
AuthorizedAPI,
payload?.slug,
payload?.data
);
if (response?.status === 200) {
yield put(
WarehouseLocationsActions.locationSuccess({
loader: payload?.loader,
childData: response?.data?.data
})
);
} else {
payload.onFailedLocation(response.data.error);
yield put(
WarehouseLocationsActions.locationFailure({
loader: payload?.loader,
error: response?.data
})
);
}
}
export function* onAddRequestLocation({ payload }) {
const response = yield call(
ApiServices[payload?.method],
AuthorizedAPI,
payload?.slug,
payload?.data
);
LOGGER.log('add response', response.data);
if (response?.status === 200) {
yield put(
WarehouseLocationsActions.locationSuccess({
loader: payload?.loader,
childData: {
parent: payload?.parent,
childrenData: [{ ...response?.data }]
}
})
);
} else {
payload.onFailedLocation(response.data.error);
yield put(
WarehouseLocationsActions.locationFailure({
loader: payload?.loader,
error: response?.data
})
);
}
}
export function* onEditRequestLocation({ payload }) {
const response = yield call(
ApiServices[payload?.method],
AuthorizedAPI,
payload?.slug,
payload?.data
);
LOGGER.log('edit response', response.data);
if (response?.status === 200) {
yield put(
WarehouseLocationsActions.locationSuccess({
loader: payload?.loader,
childData: {
edited: true,
child: payload?.child,
childrenData: [{ ...response?.data }]
}
})
);
} else {
payload.onFailedLocation(response.data.error);
yield put(
WarehouseLocationsActions.locationFailure({
loader: payload?.loader,
error: response?.data
})
);
}
}
export default [
takeEvery(WarehouseLocationsTypes.LOCATION_REQUEST, onRequestLocation),
takeEvery(WarehouseLocationsTypes.ADD_LOCATION_REQUEST, onAddRequestLocation),
takeEvery(WarehouseLocationsTypes.EDIT_LOCATION_REQUEST, onEditRequestLocation)
];

View File

@@ -5,6 +5,7 @@ import UsersSaga from './Users';
import ProductSaga from './Product';
import InventorySaga from './Inventory';
import RolesSaga from './Roles';
import WarehouseLocationsSaga from './WarehouseLocations';
export default function* rootSaga() {
yield all([...AuthSaga]);
@@ -13,4 +14,5 @@ export default function* rootSaga() {
yield all([...ProductSaga]);
yield all([...InventorySaga]);
yield all([...RolesSaga]);
yield all([...WarehouseLocationsSaga]);
}

View File

@@ -0,0 +1,131 @@
import { API } from 'constant';
import LOGGER from 'services/Logger';
export const getChildLocationType = (parentLocationType) => {
switch (parentLocationType) {
case 'warehouse':
return 'zone';
case 'zone':
return 'area';
case 'area':
return 'row';
case 'row':
return 'bay';
case 'bay':
return 'level';
case 'level':
return 'sublevel';
case 'sublevel':
return 'sublevel';
default:
return 'unknown';
}
};
export const getAPIslugOfLocationType = (locationType) => {
switch (locationType) {
case 'zone':
return API.ADD_NEW_ZONE;
case 'area':
return API.ADD_NEW_AREA;
case 'row':
return API.ADD_NEW_ROW;
case 'bay':
return API.ADD_NEW_BAY;
case 'level':
return API.ADD_NEW_LEVEL;
case 'sublevel':
return API.ADD_NEW_SUBLEVEL;
default:
throw new Error('default values returned');
}
};
export const getPropertiesOfLocationType = (locationType) => {
switch (locationType) {
case 'zone':
return ['name', 'type', 'specs'];
case 'area':
return ['name', 'type', 'specs'];
case 'row':
return ['name', 'number', 'specs'];
case 'bay':
return ['name', 'type', 'number', 'specs'];
case 'level':
return ['name', 'type', 'specs'];
case 'sublevel':
return ['name', 'specs'];
default:
LOGGER.error('default values returned');
return ['name', 'type', 'specs'];
}
};
export const getColorOfLocationType = (locationType) => {
switch (locationType) {
case 'zone':
return '#9b5de5';
case 'area':
return '#f15bb5';
case 'row':
return '#fee440';
case 'bay':
return '#00bbf9';
case 'level':
return '#00f5d4';
default:
return '#555555';
}
};
export const toTitleCase = (str) => {
return str.replace(/\w\S*/g, function (txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
};
export const getInitialvaluesFromData = (data) => {
const properties = getPropertiesOfLocationType(data.location);
const mapper = {};
properties.forEach((prop) => (mapper[prop] = data[prop] || ''));
if (data.location === 'sublevel') {
mapper['type'] = data['type'];
mapper['parentIsLevel'] = data['current_depth'] === 1;
mapper['parent_id'] =
data['current_depth'] === 1 ? data['main_level_id'] : data['parent_sublevel_id'];
// mapper['positions'] = data['positions'] || [];
}
// LOGGER.log('init form values', mapper);
return mapper;
};
export const getInitialvaluesFromParentData = (data) => {
const childType = getChildLocationType(data.location);
const properties = getPropertiesOfLocationType(childType);
const mapper = {};
properties.forEach((prop) => (mapper[prop] = ''));
if (childType === 'sublevel') {
mapper['parent_id'] = data.id;
mapper['parentIsLevel'] = data.location === 'level' ? true : false;
mapper['positions'] = [];
}
// LOGGER.log('init form values', mapper);
return mapper;
};