Untitled
unknown
plain_text
2 years ago
26 kB
4
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...