Untitled
unknown
plain_text
3 years ago
26 kB
7
Indexable
import {
AsyncButton,
Column,
DataGrid,
DeleteSVG,
Form,
FormTitle,
Image,
LogSVG,
makeStyles,
QueryParameters,
showConfirm,
TableContextProps,
TextInput,
useBreadcrumb,
} from '@jubelio/components';
import { api, useTranslation } from '@jubelio/core-lib';
import { Button, Card, CardContent, CircularProgress, Divider, Grid, IconButton } from '@mui/material';
import React from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import BarcodeNext from '../../../../../assets/img/barcode-next.jpg';
import HighlightOffIcon from '@mui/icons-material/HighlightOff';
import { Close } from '@mui/icons-material';
import Joi from 'joi';
import { ChecklistSVG } from '../../../../../assets/icons/ic_checklist';
import { TrxHistory } from '../../../../../components/TrxHistory';
import SplitShelfPickingModal from '../../components/split-shelf-picking';
interface IBin {
bin_id?: number;
location_id?: number;
bin_final_code?: string;
}
interface IPickList {
picklist_id: number;
picklist_no: string;
note: string;
is_completed: boolean;
is_warehouse: boolean;
items: Array<ItemsPickList>;
}
interface ItemsPickList extends IBin {
bin_id: number | null;
bundle_item_id: number;
item_id: number;
location_id: number;
picklist_detail_id: number;
qty_ordered: number;
qty_picked: number;
salesorder_detail_id: number;
salesorder_id: number;
invoice_no: number | null;
update: boolean;
item_code?: string;
}
const schema = Joi.object({
stack_code: Joi.string().allow(''),
notes: Joi.string().allow(''),
});
const defaultValue = {
stack_code: '',
notes: '',
};
let formHook;
export const OnProcessView: React.FC = () => {
const classes = useStyles();
const { setTitle, setHide } = useBreadcrumb();
const { id } = useParams<{ id: string }>();
const { t } = useTranslation('warehouse');
const navigate = useNavigate();
const { state, pathname } = useLocation();
const pathArray = pathname.split('/');
const { pick_id, pick_no } = state || {};
const [completed, setCompleted] = React.useState(false);
const [busy, setBusy] = React.useState(false);
const [openCancel, setOpenCancel] = React.useState<boolean>(false);
const [openTrxHistory, setOpenTrxHistory] = React.useState<boolean>(false);
const [picklist, setPickList] = React.useState<IPickList>();
const [bin, setBin] = React.useState<IBin>({});
const picklistItems = React.useRef<any>([]);
const gridContextRef = React.useRef<TableContextProps>();
const rakInputRef = React.useRef(null);
const skuInputRef = React.useRef(null);
const [modalDetail, setModalDetail] = React.useState(null);
/**
* fetcher
*/
const getTrxHistory = async (options: QueryParameters) => {
const res = await api.get(`activities/history/`, {
trx_id: id,
type: 'pil',
sortBy: 'log_id',
sortDirection: 'desc',
...options,
});
return res;
};
const getPicklistDetail = async () => {
const res = await api.get(`sales/picklists/${id}`);
const location_id = res.items[0]['location_id'];
const bin = await api.get(`wms/default-bin/${location_id}`);
setBin(bin);
picklistItems.current = res.items;
setCompleted(res.is_completed);
setPickList(res);
return res;
};
const getRak = async (options) => {
const res = await api.get(`locations/bin/${-1}`, {
sourcebin: 'picking',
sortBy: 'bin_id',
sortDirection: 'desc',
...options,
});
return res;
};
const getFormHooks = (hooks) => {
formHook = hooks;
};
const setGridContext = (context: TableContextProps) => {
gridContextRef.current = context;
};
let countChange = 0;
const rowChange = (value, item, data, col, row) => {
if (item) {
if (col === 'qty_picked' && value !== null) item.qty_picked = value;
if (col === 'bin' && value !== null && value instanceof Object) {
item.bin_id = value.bin_id;
item.bin_final_code = value.bin_final_code;
skuInputRef.current.focus();
}
}
if (countChange === 0) {
updateItems(item, undefined, true, row);
countChange++;
} else {
countChange = 0;
}
picklistItems.current = [...data];
};
const updateItems = async (value, item_code?, rowChange = false, idx?, finish?) => {
let index = idx;
let payload: IPickList;
try {
if (finish) {
const items = picklist.items.map((item) => {
return {
bin_id: bin.bin_id,
bundle_item_id: item.bundle_item_id,
item_id: item.item_id,
location_id: item.location_id,
picklist_detail_id: item.picklist_detail_id,
qty_ordered: item.qty_ordered,
qty_picked: item.qty_ordered,
salesorder_detail_id: item.salesorder_detail_id,
salesorder_id: item.salesorder_id,
invoice_no: item.invoice_no,
update: true,
};
});
payload = {
picklist_id: picklist.picklist_id,
picklist_no: picklist.picklist_no,
note: formHook.getValues('notes') || picklist.note,
is_completed: true,
is_warehouse: true,
items: items,
};
setCompleted(true);
setPickList((prev) => {
picklistItems.current = [...items];
return { ...prev, items: [...items] };
});
} else {
if (!rowChange) {
/**
*first, please find index in itemsgrid,
*if item_code same as value from sku then excute sell-to-barcode
*/
index = picklist.items.findIndex((item) => item.item_code === item_code);
if (index === -1) {
return alert('ga nemu item code');
}
}
setPickList((prev) => {
prev.items[index].qty_picked =
value.qty_picked === picklist.items[index].qty_picked
? value.qty_picked
: picklist.items[index].qty_picked + 1;
prev.items[index].bin_final_code = value.bin_final_code || bin.bin_final_code;
prev.items[index].bin_id = value.bin_id || bin.bin_id;
//change picklist items, requirement progressbar
picklistItems.current = [...prev.items];
return { ...prev };
});
/**
* mapping items, then change data with index
*/
const items = picklist.items.map((item) => {
return {
bin_id: item.bin_id || null,
bundle_item_id: item.bundle_item_id,
item_id: item.item_id,
location_id: item.location_id,
picklist_detail_id: item.picklist_detail_id,
qty_ordered: item.qty_ordered,
qty_picked: item.qty_picked || 0,
salesorder_detail_id: item.salesorder_detail_id,
salesorder_id: item.salesorder_id,
invoice_no: item.invoice_no,
update: false,
};
});
items[index].bin_id = value.bin_id || bin.bin_id;
items[index].qty_picked =
value.qty_picked === picklist.items[index].qty_picked
? value.qty_picked
: picklist.items[index].qty_picked + 1;
items[index].update = true;
/**
* @is_completed check every all array have been changed by the action enter
*
*/
const is_completed = items.every((item) => item.qty_picked === item.qty_ordered);
setCompleted(is_completed);
payload = {
picklist_id: picklist.picklist_id,
picklist_no: picklist.picklist_no,
note: formHook.getValues('notes') || picklist.note,
is_completed: is_completed,
is_warehouse: true,
items: items,
};
}
const res = await api.post(`wms/sales/picklists/`, { ...payload });
formHook.reset();
return res;
} finally {
formHook.setValue('sku', '');
formHook.setValue('stack_code', '');
setBusy(false);
}
};
const handleKeyDown = async (e) => {
if (e.key === 'Enter') {
setBusy(true);
const value = e.target.value;
const name = e.target.name;
if (!value) return;
if (name == 'stack_code') {
try {
const res = await api.get(`wms/scan-bin/${value}`);
if (res) {
skuInputRef.current.focus();
setBin(res);
}
formHook.reset();
return res;
} catch (error) {
formHook.setError('stack_code', { type: 'manual', message: t('data_not_found') });
rakInputRef.current.focus();
} finally {
formHook.setValue('stack_code', '');
setBusy(false);
}
} else if (name == 'sku') {
updateItems({}, value);
}
}
};
const renderRak = (props) => {
const data = props.data || props.cell.row.original;
return (
<div className="flex-row d-flex align-items-center">
<div className={classes.fontTable}>
<div>{data.bin_final_code}</div>
</div>
</div>
);
};
/**
* fetch api to get form data if it's not a new form
* called on React.useEffect when initializing
*/
const renderProductView = (props) => {
const data = props.data || props.cell.row.original;
return (
<div className="flex-row d-flex align-items-center">
<div className="mr-3">
<Image src={data.thumbnail} className="d-50 rounded" />
</div>
<div className={classes.productText}>
<div className={`${classes.fontTable} text-second`}>{data.item_full_name ?? data.item_short_name}</div>
<div className={`${classes.fontTable} text-second`}>{data.item_code}</div>
</div>
</div>
);
};
const progressBarCell = () => {
if (!picklist?.items) return;
const fromNo = picklist.items.reduce((acc, next) => Number(acc) + Number(next.qty_picked), 0);
const toNo = picklist.items.reduce((acc, next) => Number(acc) + Number(next.qty_ordered), 0);
const progress = (fromNo / toNo) * 100;
return (
<div>
<div>{`${fromNo} / ${toNo} QTY`}</div>
<div className={classes.progress}>
<div
style={{
width: `${progress}%`,
backgroundColor: '#4CAF50',
borderRadius: '5px',
height: '8px',
}}
/>
<div
style={{
width: `${100 - progress}%`,
backgroundColor: '#ECEFF1',
borderRadius: '5px',
height: '8px',
}}
/>
</div>
</div>
);
};
const columns: Column[] = [
{
header: <div className="d-flex justify-content-start w-100 text-second">{t('product')}</div>,
binding: 'item_full_name',
headerClassName: `${classes.colVariant} font-size-md font-weight-semibold`,
editor: renderProductView,
freezeCol: true,
readOnly: true,
width: 350,
},
{
header: <div className="d-flex justify-content-end w-100 text-second">Qty Pesan</div>,
binding: 'qty_ordered',
format: 'number',
readOnly: true,
width: 100,
},
{
header: <div className="w-100 text-second">{t('orders.picking.on_process.stack_code')}</div>,
binding: 'bin',
width: 150,
format: 'dropdown',
readOnly: completed,
options: {
data: getRak,
placeholder: 'Ketik untuk mencari produk',
value: 'bin_id', // where to save the selected value in the grid item
itemFormatter: renderRak,
displayFrom: 'bin_final_code', // where to save the display text in the grid item
label: 'bin_final_code', // where to get the display text from selected list value
itemHeight: 50,
},
editor: renderRak,
},
{
header: <div className="d-flex justify-content-end w-100 text-second">Qty Ambil</div>,
binding: 'qty_picked',
format: 'number',
readOnly: completed,
required: true,
width: 150,
},
{
header: <div className="w-100 text-second text-center">Serial/Batch</div>,
binding: 'serial_no',
editor: (props) => {
const data = props.data || props.cell.row.original;
return (
<div className="d-flex justify-content-center w-100">
<AsyncButton color="primary" variant="contained" type="button" onClick={async () => alert('lihat')}>
Lihat
</AsyncButton>
</div>
);
},
readOnly: true,
width: 150,
},
{
header: <div className="w-100 text-second">{t('orders.order_no')}</div>,
binding: 'salesorder_no',
format: 'string',
readOnly: true,
width: 200,
},
{
header: <div className="w-100 text-second">Status</div>,
binding: 'channel_status',
format: 'string',
readOnly: true,
width: 150,
},
{
header: <div className="d-flex justify-content-center w-100 text-second">{t('orders.ref_no')}</div>,
binding: 'tracking_no',
format: 'string',
width: 150,
readOnly: true,
editor: (props) => {
const data = props.data || props.cell.row.original;
return (
<div className="d-flex justify-content-center w-100">
{data.tracking_no ? <ChecklistSVG fill="#4CAF50" /> : <HighlightOffIcon sx={{ color: '#ed5565' }} />}
</div>
);
},
},
...(pathArray.at(3) === 'picking'
? [
{
header: <div className="d-flex justify-content-center w-100">{t('action')}</div>,
binding: 'action',
width: 150,
readOnly: true,
editor: ({ cell }) => {
const { original } = cell.row;
return (
<div className="d-flex justify-content-center w-100">
<Button
className="mr-1 bg-primary"
type="button"
color="primary"
variant="contained"
onClick={() =>
setModalDetail({ ...original, qty_in_base: original.qty_ordered, returnDetail: picklist })
}
>
{t('Pecah Rask')}
</Button>
</div>
);
},
},
]
: []),
{
binding: 'delete',
header: t(''),
required: false,
width: 50,
resizable: true,
readOnly: true,
editor: (props) => {
const { row } = props.cell;
const deleteRow = () => {
setBusy(true);
picklistItems.current = picklistItems.current.filter((item, index) => index != row.id);
setTimeout(() => {
setBusy(false);
}, 50);
};
return (
<div className={`${classes.inputWrapper} bg-white d-flex align-items-center`}>
<IconButton onClick={deleteRow}>
{busy ? <CircularProgress size={12} /> : <DeleteSVG height={18} width={18} className="text-danger" />}
</IconButton>
</div>
);
},
},
];
function resetBreadcrumb() {
setTitle(null);
setHide(null);
}
const onFinishPick = async () => {
updateItems({}, null, null, null, true);
showConfirm({
title: 'Sedang mencetak faktur pesanan.',
subtitle: 'Silahkan cek di daftar pesanan secara berkala, untuk memastikan faktur telah tercetak.',
onConfirm: async () => {
navigate(pathArray.slice(0, 4).join('/'));
},
confirmText: t('close'),
confirmColor: 'primary',
showCancel: false,
sizeAlert: 'xs',
});
};
const handleClose = () => {
if (pathArray.at(3) === 'packing') {
navigate('/warehouse/orders/packing', { state: { activeTab: 'not-started' } });
} else {
navigate('/warehouse/orders/picking', { state: { activeTab: 'process' } });
}
};
const toggleCancel = () => {
setOpenCancel(!openCancel);
};
const toggleTrxHistory = () => {
setOpenTrxHistory(!openTrxHistory);
return null;
};
const titlePage = React.useMemo(() => {
let title = '';
if (pathArray.at(3) === 'packing') {
title = 'Packing';
} else {
title = 'Picking';
}
return title;
}, [id]);
React.useEffect(() => {
if (!pick_id || !pick_no) {
navigate(pathArray.slice(0, 4).join('/'), { state: { activeTab: 'process' } });
} else {
getPicklistDetail();
}
return resetBreadcrumb;
}, []);
React.useEffect(() => {
setHide([pathArray.slice(0, 6).join('/')]);
setTitle([{ path: pathname, name: `${titlePage} - ${pick_no}` }]);
}, [picklist]);
return (
<>
<TrxHistory fetcher={getTrxHistory} open={openTrxHistory} close={toggleTrxHistory} type="trx" />
<Card className="margin-card h-form">
<FormTitle maxWidth inCard title={t(`${titlePage} - ${pick_no}`)}>
<AsyncButton
type="button"
icon={<Close style={{ height: 18, width: 18 }} />}
color="error"
variant="contained"
className="mr-1 bg-danger"
onClick={async () => handleClose()}
busy={busy}
>
{t('close')}
</AsyncButton>
<AsyncButton
type="button"
color="inherit"
variant="outlined"
className="mr-1"
title={t('history')}
onClick={toggleTrxHistory}
busy={busy}
>
<LogSVG height={18} width={18} className="mr-2" /> {t('history')}
</AsyncButton>
{pathArray.at(3) !== 'packing' && (
<AsyncButton
startIcon={<ChecklistSVG height={18} className="text-white" />}
type="button"
onClick={async () => onFinishPick()}
color="primary"
busy={busy}
disabled={completed}
>
{t('orders.picking.on_process.finish_pick')}
</AsyncButton>
)}
</FormTitle>
<Form schema={schema} values={defaultValue} getFormHooks={getFormHooks}>
<Grid container className="d-flex px-4 pt-3 mt-2 justify-content-between">
<Grid item md={5}>
<Grid container className="d-flex align-items-center" spacing={{ xs: 1, md: 4 }}>
<Grid item md={4}>
<span className="text-second font-weight-bold">{t('orders.picking.on_process.stack_code')}</span>
</Grid>
<Grid item md={8}>
<TextInput
inputRef={rakInputRef}
label=""
name="stack_code"
placeholder={'Scan kode rak'}
margin={'dense'}
onKeyDown={handleKeyDown}
disabled={completed}
InputProps={{
sx: {
'& .MuiOutlinedInput-notchedOutline': {
bgcolor: completed ? 'rgb(0,0,0,0.05)' : 'none',
},
},
}}
/>
</Grid>
</Grid>
</Grid>
<Grid item md={6}>
<Grid
classes={{ root: classes.card }}
container
className="d-flex justify-content-end align-items-center"
spacing={{ xs: 1, md: 4 }}
>
<Grid item md={6}>
<div className="d-flex flex-column card-wrapper">
<Card className="d-flex justify-content-center py-1">
<AsyncButton
className="font-weight-bold"
fullWidth
color="inherit"
type="button"
onClick={() => rakInputRef.current.focus()}
disabled={completed}
>
{t('orders.picking.on_process.next_move')}
</AsyncButton>
</Card>
<Card className="d-flex justify-content-center">
<CardContent className="d-flex flex-column" sx={{ gap: '10px' }}>
<span className="">{t('orders.picking.on_process.scan_here')}</span>
<Image src={BarcodeNext} width={260} height={45} className="mb-2" />
</CardContent>
</Card>
</div>
</Grid>
</Grid>
</Grid>
<Divider light variant="fullWidth" orientation="horizontal" className="w-100 my-5" />
<Grid item md={5}>
<Grid container className="d-flex align-items-center" spacing={{ xs: 1, md: 4 }}>
<Grid item md={6}>
<span className={`text-second font-weight-bold ${classes.stackText}`}>
{`${t('orders.picking.on_process.active_stack')}: ${bin.bin_final_code || ''}`}
</span>
</Grid>
<Grid item md={6}>
{progressBarCell()}
</Grid>
</Grid>
</Grid>
<Grid item md={5}>
<Grid container className="d-flex" spacing={{ xs: 1, md: 4 }}>
<Grid item md={4} className="mt-1">
<span className="text-second">{t('notes')}</span>
</Grid>
<Grid item md={8}>
<TextInput
multiline
label=""
name="notes"
placeholder={'Masukkan keterangan'}
margin={'dense'}
rows={3}
disabled={completed}
InputProps={{
sx: {
'& .MuiOutlinedInput-notchedOutline': {
bgcolor: completed ? 'rgb(0,0,0,0.05)' : 'none',
},
},
}}
/>
</Grid>
</Grid>
</Grid>
<Grid item md={5} className="my-2">
<TextInput
inputRef={skuInputRef}
label=""
name="sku"
placeholder={t('orders.picking.on_process.input_sku')}
margin={'dense'}
onKeyDown={handleKeyDown}
disabled={completed}
InputProps={{
sx: {
'& .MuiOutlinedInput-notchedOutline': {
bgcolor: completed ? 'rgb(0,0,0,0.05)' : 'none',
},
},
}}
/>
</Grid>
<Grid item md={12} className={'mb-5'}>
<DataGrid
allowDelete
columns={columns}
data={picklistItems.current}
itemHeight={50}
selectorColor="rgb(102, 204, 255)"
frozenColumn={1}
havePagination={picklistItems.current.length > 25}
setContext={setGridContext}
onChanged={rowChange}
/>
</Grid>
</Grid>
</Form>
</Card>
{modalDetail && <SplitShelfPickingModal detail={modalDetail} setModalDetail={setModalDetail} />}
</>
);
};
const useStyles = makeStyles(() => ({
card: {
'& .MuiCard-root': {
backgroundColor: '#E5F3FF',
'& .MuiButton-root': {
backgroundColor: '#E5F3FF',
},
},
'& .MuiCardContent-root': {
padding: '12px 0 !important',
},
'& .card-wrapper': {
gap: '10px',
},
},
stackText: {
fontSize: '20px',
},
productText: {
display: 'table',
tableLayout: 'fixed',
width: '100%',
textAlign: 'start',
},
fontTable: {
fontSize: '12px',
width: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
textAlign: 'start',
},
fontDesc: {
fontSize: '12px',
width: '100px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
textAlign: 'start',
},
colVariant: {
minWidth: '300px',
backgroundColor: '#fff',
zIndex: 1,
},
iconTransform: {
width: '18px',
display: 'flex',
height: '18px',
alignItems: 'center',
justifyContent: 'center',
transform: 'rotate(90deg)',
},
inputWrapper: {
// border: '1px solid rgb(230, 233, 239)',
width: '135%',
margin: '-22px -6px',
borderRadius: 4,
padding: 4,
backgroundColor: '#fff',
},
inputWrapperAll: {
'& .MuiAccordion-root': {
margin: '20px 20px',
'& .MuiAccordionSummary-gutters': {
borderBottom: '1px solid rgb(230, 233, 239)',
borderRadius: 0,
padding: '0px',
},
'&::before': {
display: 'none',
},
'& .MuiAccordionDetails-root': {
padding: 0,
},
},
},
image: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'& img': {
maxHeight: 40,
},
},
progress: {
width: '100%',
display: 'inline-flex',
},
}));
Editor is loading...