Untitled

 avatar
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...