|
- "use client"
- import { SearchJoResultRequest, fetchJos, updateJo } from "@/app/api/jo/actions";
- import React, { useCallback, useEffect, useMemo, useState } from "react";
- import { useTranslation } from "react-i18next";
- import { Criterion } from "../SearchBox";
- import SearchResults, { Column, defaultPagingController } from "../SearchResults/SearchResults";
- import { EditNote } from "@mui/icons-material";
- import { arrayToDateString, arrayToDateTimeString, integerFormatter } from "@/app/utils/formatUtil";
- import { orderBy, uniqBy, upperFirst } from "lodash";
- import SearchBox from "../SearchBox/SearchBox";
- import { useRouter } from "next/navigation";
- import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form";
- import { StockInLineInput } from "@/app/api/stockIn";
- import { JobOrder, JoDetailPickLine, JoStatus } from "@/app/api/jo";
- import { Button, Stack } from "@mui/material";
- import { BomCombo } from "@/app/api/bom";
- import JoCreateFormModal from "./JoCreateFormModal";
- import AddIcon from '@mui/icons-material/Add';
- import QcStockInModal from "../PoDetail/QcStockInModal";
- import { useSession } from "next-auth/react";
- import { SessionWithTokens } from "@/config/authConfig";
- import { createStockInLine } from "@/app/api/stockIn/actions";
- import { msg } from "../Swal/CustomAlerts";
- import dayjs from "dayjs";
-
- import { fetchInventories } from "@/app/api/inventory/actions";
- import { InventoryResult } from "@/app/api/inventory";
- import { PrinterCombo } from "@/app/api/settings/printer";
-
- interface Props {
- defaultInputs: SearchJoResultRequest,
- bomCombo: BomCombo[]
- printerCombo: PrinterCombo[];
- }
-
- type SearchQuery = Partial<Omit<JobOrder, "id">>;
-
- type SearchParamNames = keyof SearchQuery;
-
- const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo }) => {
- const { t } = useTranslation("jo");
- const router = useRouter()
- const [filteredJos, setFilteredJos] = useState<JobOrder[]>([]);
- const [inputs, setInputs] = useState(defaultInputs);
- const [pagingController, setPagingController] = useState(
- defaultPagingController
- )
- const [totalCount, setTotalCount] = useState(0)
- const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false)
-
- // console.log(inputs)
- const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]);
-
- const [detailedJos, setDetailedJos] = useState<Map<number, JobOrder>>(new Map());
-
- const fetchJoDetailClient = async (id: number): Promise<JobOrder> => {
- const response = await fetch(`/api/jo/detail?id=${id}`);
- if (!response.ok) {
- throw new Error('Failed to fetch JO detail');
- }
- return response.json();
- };
-
- useEffect(() => {
- const fetchDetailedJos = async () => {
- const detailedMap = new Map<number, JobOrder>();
-
- for (const jo of filteredJos) {
- try {
- const detailedJo = await fetchJoDetailClient(jo.id); // Use client function
- detailedMap.set(jo.id, detailedJo);
- } catch (error) {
- console.error(`Error fetching detail for JO ${jo.id}:`, error);
- }
- }
-
- setDetailedJos(detailedMap);
- };
-
- if (filteredJos.length > 0) {
- fetchDetailedJos();
- }
- }, [filteredJos]);
-
- useEffect(() => {
- const fetchInventoryData = async () => {
- try {
- const inventoryResponse = await fetchInventories({
- code: "",
- name: "",
- type: "",
- pageNum: 0,
- pageSize: 1000
- });
- setInventoryData(inventoryResponse.records);
- } catch (error) {
- console.error("Error fetching inventory data:", error);
- }
- };
-
- fetchInventoryData();
- }, []);
-
- const getStockAvailable = (pickLine: JoDetailPickLine) => {
- const inventory = inventoryData.find(inventory =>
- inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name
- );
-
- if (inventory) {
- return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty);
- }
-
- return 0;
- };
-
- const isStockSufficient = (pickLine: JoDetailPickLine) => {
- const stockAvailable = getStockAvailable(pickLine);
- return stockAvailable >= pickLine.reqQty;
- };
-
- const getStockCounts = (jo: JobOrder) => {
- const detailedJo = detailedJos.get(jo.id);
-
- if (!detailedJo?.pickLines || detailedJo.pickLines.length === 0) {
- return { total: 0, sufficient: 0, insufficient: 0 };
- }
-
- const totalLines = detailedJo.pickLines.length;
- const sufficientLines = detailedJo.pickLines.filter(pickLine => isStockSufficient(pickLine)).length;
- const insufficientLines = totalLines - sufficientLines;
-
- return {
- total: totalLines,
- sufficient: sufficientLines,
- insufficient: insufficientLines
- };
- };
-
- const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => [
- { label: t("Code"), paramName: "code", type: "text" },
- { label: t("Item Name"), paramName: "itemName", type: "text" },
- { label: t("Plan Start"), label2: t("Plan Start To"), paramName: "planStart", type: "datetimeRange" },
- ], [t])
-
- const columns = useMemo<Column<JobOrder>[]>(
- () => [
- {
- name: "code",
- label: t("Code"),
- flex: 2
- },
- {
- name: "item",
- label: t("Item Code"),
- renderCell: (row) => {
- return row.item ? t(row.item.code) : '-'
- }
- },
- {
- name: "itemName",
- label: t("Item Name"),
- renderCell: (row) => {
- return row.item ? t(row.item.name) : '-'
- }
- },
- {
- name: "reqQty",
- label: t("Req. Qty"),
- align: "right",
- headerAlign: "right",
- renderCell: (row) => {
- return integerFormatter.format(row.reqQty)
- }
- },
- {
- name: "item",
- label: t("UoM"),
- align: "left",
- headerAlign: "left",
- renderCell: (row) => {
- return row.item?.uom ? t(row.item.uom.udfudesc) : '-'
- }
- },
- {
- name: "status",
- label: t("Status"),
- renderCell: (row) => { // TODO improve
- return <span style={{color: row.stockInLineStatus == "escalated" ? "red" : "inherit"}}>
- {t(upperFirst(row.status))}
- </span>
- }
- },{
- name: "planStart",
- label: t("Estimated Production Date"),
- align: "left",
- headerAlign: "left",
- renderCell: (row) => {
- return row.planStart ? arrayToDateTimeString(row.planStart) : '-'
- }
- },
- {
- name: "stockStatus" as keyof JobOrder,
- label: t("BOM Status"),
- align: "left",
- headerAlign: "left",
- renderCell: (row) => {
- const stockCounts = getStockCounts(row);
- return (
- <span style={{ color: stockCounts.insufficient > 0 ? 'red' : 'green' }}>
- {stockCounts.sufficient}/{stockCounts.total}
- </span>
- );
- }
- },
- {
- // TODO put it inside Action Buttons
- name: "id",
- label: t("Actions"),
- // onClick: (record) => onDetailClick(record),
- // buttonIcon: <EditNote />,
- renderCell: (row) => {
- const btnSx = getButtonSx(row);
- return (
- <Button
- id="emailSupplier"
- type="button"
- variant="contained"
- color="primary"
- sx={{ width: "150px", backgroundColor: btnSx.color }}
- // disabled={params.row.status != "rejected" && params.row.status != "partially_completed"}
- onClick={() => onDetailClick(row)}
- >{btnSx.label}</Button>
- )
- }
- },
- ], [inventoryData]
- )
-
- const handleUpdate = useCallback(async (jo: JobOrder) => {
- console.log(jo);
- try {
- // setIsUploading(true)
- if (jo.id) {
- const response = await updateJo({ id: jo.id, status: "storing" });
- console.log(`%c Updated JO:`, "color:lime", response);
- const postData = {
- itemId: jo?.item?.id!!,
- acceptedQty: jo?.reqQty ?? 1,
- productLotNo: jo?.code,
- productionDate: arrayToDateString(dayjs(), "input"),
- jobOrderId: jo?.id,
- // acceptedQty: secondReceiveQty || 0,
- // acceptedQty: row.acceptedQty,
- };
- const res = await createStockInLine(postData);
- console.log(`%c Created Stock In Line`, "color:lime", res);
- msg(t("update success"));
- refetchData(defaultInputs, "search");
- }
-
- } catch (e) {
- // backend error
- // setServerError(t("An error has occurred. Please try again later."));
- console.log(e);
- } finally {
- // setIsUploading(false)
- }
- }, [])
-
- const refetchData = useCallback(async (
- query: Record<SearchParamNames, string> | SearchJoResultRequest,
- actionType: "reset" | "search" | "paging",
- ) => {
- const params: SearchJoResultRequest = {
- code: query.code,
- itemName: query.itemName,
- planStart: query.planStart,
- planStartTo: query.planStartTo,
- pageNum: pagingController.pageNum - 1,
- pageSize: pagingController.pageSize,
- }
- const response = await fetchJos(params)
-
- if (response) {
- setTotalCount(response.total);
- switch (actionType) {
- case "reset":
- case "search":
- setFilteredJos(() => orderBy(response.records, ["id"], ["desc"]));
- break;
- case "paging":
- setFilteredJos((fs) =>
- orderBy(uniqBy([...fs, ...response.records], "id"), ["id"], ["desc"]),
- );
- break;
- }
- }
- }, [pagingController, setPagingController])
-
- const searchDataByPage = useCallback(() => {
- refetchData(inputs, "paging");
- }, [inputs])
- useEffect(() => {
- searchDataByPage();
- }, [pagingController]);
-
- const getButtonSx = (jo : JobOrder) => { // TODO put it in ActionButtons.ts
- const joStatus = jo.status?.toLowerCase();
- const silStatus = jo.stockInLineStatus?.toLowerCase();
- let btnSx = {label:"", color:""};
- switch (joStatus) {
- case "planning": btnSx = {label: t("release jo"), color:"primary.main"}; break;
- case "pending": btnSx = {label: t("scan picked material"), color:"error.main"}; break;
- case "processing": btnSx = {label: t("complete jo"), color:"warning.main"}; break;
- // case "packaging":
- // case "storing": btnSx = {label: t("view putaway"), color:"secondary.main"}; break;
- case "storing":
- switch (silStatus) {
- case "pending": btnSx = {label: t("process epqc"), color:"success.main"}; break;
- case "received": btnSx = {label: t("view putaway"), color:"secondary.main"}; break;
- case "escalated":
- if (sessionToken?.id == jo.silHandlerId) {
- btnSx = {label: t("escalation processing"), color:"warning.main"};
- break;
- }
- default: btnSx = {label: t("view stockin"), color:"info.main"};
- }
- break;
- case "completed": btnSx = {label: t("view stockin"), color:"info.main"}; break;
- default: btnSx = {label: t("scan picked material"), color:"success.main"};
- }
- return btnSx
- };
-
- const { data: session } = useSession();
- const sessionToken = session as SessionWithTokens | null;
-
- const [openModal, setOpenModal] = useState<boolean>(false);
- const [modalInfo, setModalInfo] = useState<StockInLineInput>();
-
- const onDetailClick = useCallback((record: JobOrder) => {
-
- if (record.status == "processing") {
- handleUpdate(record)
- } else if (record.status == "storing" || record.status == "completed") {
- if (record.stockInLineId != null) {
- const data = {
- id: record.stockInLineId,
- expiryDate: arrayToDateString(dayjs().add(1, "month"), "input"),
- }
- setModalInfo(data);
- setOpenModal(true);
- } else { alert('Invalid Stock In Line Id'); }
- } else {
- router.push(`/jo/edit?id=${record.id}`)
- }
- }, [])
-
- const closeNewModal = useCallback(() => {
- // const response = updateJo({ id: 1, status: "storing" });
- setOpenModal(false); // Close the modal first
- // setTimeout(() => {
- // }, 300); // Add a delay to avoid immediate re-trigger of useEffect
- refetchData(defaultInputs, "search");
- }, []);
-
- const onSearch = useCallback((query: Record<SearchParamNames, string>) => {
- setInputs(() => ({
- code: query.code,
- itemName: query.itemName,
- planStart: query.planStart,
- planStartTo: query.planStartTo
- }))
- refetchData(query, "search");
- }, [])
-
- const onReset = useCallback(() => {
- refetchData(defaultInputs, "paging");
- }, [])
-
- // Manual Create Jo Related
-
- const onOpenCreateJoModal = useCallback(() => {
- setIsCreateJoModalOpen(() => true)
- }, [])
-
- const onCloseCreateJoModal = useCallback(() => {
- setIsCreateJoModalOpen(() => false)
- }, [])
-
- return <>
- <Stack
- direction="row"
- justifyContent="flex-end"
- spacing={2}
- sx={{ mt: 2 }}
- >
- <Button
- variant="outlined"
- startIcon={<AddIcon />}
- onClick={onOpenCreateJoModal}
- >
- {t("Create Job Order")}
- </Button>
- </Stack>
- <SearchBox
- criteria={searchCriteria}
- onSearch={onSearch}
- onReset={onReset}
- />
- <SearchResults<JobOrder>
- items={filteredJos}
- columns={columns}
- setPagingController={setPagingController}
- pagingController={pagingController}
- totalCount={totalCount}
- // isAutoPaging={false}
- />
- <JoCreateFormModal
- open={isCreateJoModalOpen}
- bomCombo={bomCombo}
- onClose={onCloseCreateJoModal}
- onSearch={searchDataByPage}
- />
-
- <QcStockInModal
- session={sessionToken}
- open={openModal}
- onClose={closeNewModal}
- inputDetail={modalInfo}
- printerCombo={printerCombo}
- // skipQc={true}
- />
- </>
- }
-
- export default JoSearch;
|