FPSMS-frontend
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.
 
 

437 рядки
16 KiB

  1. "use client"
  2. import { SearchJoResultRequest, fetchJos, updateJo } from "@/app/api/jo/actions";
  3. import React, { useCallback, useEffect, useMemo, useState } from "react";
  4. import { useTranslation } from "react-i18next";
  5. import { Criterion } from "../SearchBox";
  6. import SearchResults, { Column, defaultPagingController } from "../SearchResults/SearchResults";
  7. import { EditNote } from "@mui/icons-material";
  8. import { arrayToDateString, arrayToDateTimeString, integerFormatter } from "@/app/utils/formatUtil";
  9. import { orderBy, uniqBy, upperFirst } from "lodash";
  10. import SearchBox from "../SearchBox/SearchBox";
  11. import { useRouter } from "next/navigation";
  12. import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form";
  13. import { StockInLineInput } from "@/app/api/stockIn";
  14. import { JobOrder, JoDetailPickLine, JoStatus } from "@/app/api/jo";
  15. import { Button, Stack } from "@mui/material";
  16. import { BomCombo } from "@/app/api/bom";
  17. import JoCreateFormModal from "./JoCreateFormModal";
  18. import AddIcon from '@mui/icons-material/Add';
  19. import QcStockInModal from "../PoDetail/QcStockInModal";
  20. import { useSession } from "next-auth/react";
  21. import { SessionWithTokens } from "@/config/authConfig";
  22. import { createStockInLine } from "@/app/api/stockIn/actions";
  23. import { msg } from "../Swal/CustomAlerts";
  24. import dayjs from "dayjs";
  25. import { fetchInventories } from "@/app/api/inventory/actions";
  26. import { InventoryResult } from "@/app/api/inventory";
  27. import { PrinterCombo } from "@/app/api/settings/printer";
  28. interface Props {
  29. defaultInputs: SearchJoResultRequest,
  30. bomCombo: BomCombo[]
  31. printerCombo: PrinterCombo[];
  32. }
  33. type SearchQuery = Partial<Omit<JobOrder, "id">>;
  34. type SearchParamNames = keyof SearchQuery;
  35. const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo }) => {
  36. const { t } = useTranslation("jo");
  37. const router = useRouter()
  38. const [filteredJos, setFilteredJos] = useState<JobOrder[]>([]);
  39. const [inputs, setInputs] = useState(defaultInputs);
  40. const [pagingController, setPagingController] = useState(
  41. defaultPagingController
  42. )
  43. const [totalCount, setTotalCount] = useState(0)
  44. const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false)
  45. // console.log(inputs)
  46. const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]);
  47. const [detailedJos, setDetailedJos] = useState<Map<number, JobOrder>>(new Map());
  48. const fetchJoDetailClient = async (id: number): Promise<JobOrder> => {
  49. const response = await fetch(`/api/jo/detail?id=${id}`);
  50. if (!response.ok) {
  51. throw new Error('Failed to fetch JO detail');
  52. }
  53. return response.json();
  54. };
  55. useEffect(() => {
  56. const fetchDetailedJos = async () => {
  57. const detailedMap = new Map<number, JobOrder>();
  58. for (const jo of filteredJos) {
  59. try {
  60. const detailedJo = await fetchJoDetailClient(jo.id); // Use client function
  61. detailedMap.set(jo.id, detailedJo);
  62. } catch (error) {
  63. console.error(`Error fetching detail for JO ${jo.id}:`, error);
  64. }
  65. }
  66. setDetailedJos(detailedMap);
  67. };
  68. if (filteredJos.length > 0) {
  69. fetchDetailedJos();
  70. }
  71. }, [filteredJos]);
  72. useEffect(() => {
  73. const fetchInventoryData = async () => {
  74. try {
  75. const inventoryResponse = await fetchInventories({
  76. code: "",
  77. name: "",
  78. type: "",
  79. pageNum: 0,
  80. pageSize: 1000
  81. });
  82. setInventoryData(inventoryResponse.records);
  83. } catch (error) {
  84. console.error("Error fetching inventory data:", error);
  85. }
  86. };
  87. fetchInventoryData();
  88. }, []);
  89. const getStockAvailable = (pickLine: JoDetailPickLine) => {
  90. const inventory = inventoryData.find(inventory =>
  91. inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name
  92. );
  93. if (inventory) {
  94. return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty);
  95. }
  96. return 0;
  97. };
  98. const isStockSufficient = (pickLine: JoDetailPickLine) => {
  99. const stockAvailable = getStockAvailable(pickLine);
  100. return stockAvailable >= pickLine.reqQty;
  101. };
  102. const getStockCounts = (jo: JobOrder) => {
  103. const detailedJo = detailedJos.get(jo.id);
  104. if (!detailedJo?.pickLines || detailedJo.pickLines.length === 0) {
  105. return { total: 0, sufficient: 0, insufficient: 0 };
  106. }
  107. const totalLines = detailedJo.pickLines.length;
  108. const sufficientLines = detailedJo.pickLines.filter(pickLine => isStockSufficient(pickLine)).length;
  109. const insufficientLines = totalLines - sufficientLines;
  110. return {
  111. total: totalLines,
  112. sufficient: sufficientLines,
  113. insufficient: insufficientLines
  114. };
  115. };
  116. const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => [
  117. { label: t("Code"), paramName: "code", type: "text" },
  118. { label: t("Item Name"), paramName: "itemName", type: "text" },
  119. { label: t("Plan Start"), label2: t("Plan Start To"), paramName: "planStart", type: "datetimeRange" },
  120. ], [t])
  121. const columns = useMemo<Column<JobOrder>[]>(
  122. () => [
  123. {
  124. name: "code",
  125. label: t("Code"),
  126. flex: 2
  127. },
  128. {
  129. name: "item",
  130. label: t("Item Code"),
  131. renderCell: (row) => {
  132. return row.item ? t(row.item.code) : '-'
  133. }
  134. },
  135. {
  136. name: "itemName",
  137. label: t("Item Name"),
  138. renderCell: (row) => {
  139. return row.item ? t(row.item.name) : '-'
  140. }
  141. },
  142. {
  143. name: "reqQty",
  144. label: t("Req. Qty"),
  145. align: "right",
  146. headerAlign: "right",
  147. renderCell: (row) => {
  148. return integerFormatter.format(row.reqQty)
  149. }
  150. },
  151. {
  152. name: "item",
  153. label: t("UoM"),
  154. align: "left",
  155. headerAlign: "left",
  156. renderCell: (row) => {
  157. return row.item?.uom ? t(row.item.uom.udfudesc) : '-'
  158. }
  159. },
  160. {
  161. name: "status",
  162. label: t("Status"),
  163. renderCell: (row) => { // TODO improve
  164. return <span style={{color: row.stockInLineStatus == "escalated" ? "red" : "inherit"}}>
  165. {t(upperFirst(row.status))}
  166. </span>
  167. }
  168. },{
  169. name: "planStart",
  170. label: t("Estimated Production Date"),
  171. align: "left",
  172. headerAlign: "left",
  173. renderCell: (row) => {
  174. return row.planStart ? arrayToDateTimeString(row.planStart) : '-'
  175. }
  176. },
  177. {
  178. name: "stockStatus" as keyof JobOrder,
  179. label: t("BOM Status"),
  180. align: "left",
  181. headerAlign: "left",
  182. renderCell: (row) => {
  183. const stockCounts = getStockCounts(row);
  184. return (
  185. <span style={{ color: stockCounts.insufficient > 0 ? 'red' : 'green' }}>
  186. {stockCounts.sufficient}/{stockCounts.total}
  187. </span>
  188. );
  189. }
  190. },
  191. {
  192. // TODO put it inside Action Buttons
  193. name: "id",
  194. label: t("Actions"),
  195. // onClick: (record) => onDetailClick(record),
  196. // buttonIcon: <EditNote />,
  197. renderCell: (row) => {
  198. const btnSx = getButtonSx(row);
  199. return (
  200. <Button
  201. id="emailSupplier"
  202. type="button"
  203. variant="contained"
  204. color="primary"
  205. sx={{ width: "150px", backgroundColor: btnSx.color }}
  206. // disabled={params.row.status != "rejected" && params.row.status != "partially_completed"}
  207. onClick={() => onDetailClick(row)}
  208. >{btnSx.label}</Button>
  209. )
  210. }
  211. },
  212. ], [inventoryData]
  213. )
  214. const handleUpdate = useCallback(async (jo: JobOrder) => {
  215. console.log(jo);
  216. try {
  217. // setIsUploading(true)
  218. if (jo.id) {
  219. const response = await updateJo({ id: jo.id, status: "storing" });
  220. console.log(`%c Updated JO:`, "color:lime", response);
  221. const postData = {
  222. itemId: jo?.item?.id!!,
  223. acceptedQty: jo?.reqQty ?? 1,
  224. productLotNo: jo?.code,
  225. productionDate: arrayToDateString(dayjs(), "input"),
  226. jobOrderId: jo?.id,
  227. // acceptedQty: secondReceiveQty || 0,
  228. // acceptedQty: row.acceptedQty,
  229. };
  230. const res = await createStockInLine(postData);
  231. console.log(`%c Created Stock In Line`, "color:lime", res);
  232. msg(t("update success"));
  233. refetchData(defaultInputs, "search");
  234. }
  235. } catch (e) {
  236. // backend error
  237. // setServerError(t("An error has occurred. Please try again later."));
  238. console.log(e);
  239. } finally {
  240. // setIsUploading(false)
  241. }
  242. }, [])
  243. const refetchData = useCallback(async (
  244. query: Record<SearchParamNames, string> | SearchJoResultRequest,
  245. actionType: "reset" | "search" | "paging",
  246. ) => {
  247. const params: SearchJoResultRequest = {
  248. code: query.code,
  249. itemName: query.itemName,
  250. planStart: query.planStart,
  251. planStartTo: query.planStartTo,
  252. pageNum: pagingController.pageNum - 1,
  253. pageSize: pagingController.pageSize,
  254. }
  255. const response = await fetchJos(params)
  256. if (response) {
  257. setTotalCount(response.total);
  258. switch (actionType) {
  259. case "reset":
  260. case "search":
  261. setFilteredJos(() => orderBy(response.records, ["id"], ["desc"]));
  262. break;
  263. case "paging":
  264. setFilteredJos((fs) =>
  265. orderBy(uniqBy([...fs, ...response.records], "id"), ["id"], ["desc"]),
  266. );
  267. break;
  268. }
  269. }
  270. }, [pagingController, setPagingController])
  271. const searchDataByPage = useCallback(() => {
  272. refetchData(inputs, "paging");
  273. }, [inputs])
  274. useEffect(() => {
  275. searchDataByPage();
  276. }, [pagingController]);
  277. const getButtonSx = (jo : JobOrder) => { // TODO put it in ActionButtons.ts
  278. const joStatus = jo.status?.toLowerCase();
  279. const silStatus = jo.stockInLineStatus?.toLowerCase();
  280. let btnSx = {label:"", color:""};
  281. switch (joStatus) {
  282. case "planning": btnSx = {label: t("release jo"), color:"primary.main"}; break;
  283. case "pending": btnSx = {label: t("scan picked material"), color:"error.main"}; break;
  284. case "processing": btnSx = {label: t("complete jo"), color:"warning.main"}; break;
  285. // case "packaging":
  286. // case "storing": btnSx = {label: t("view putaway"), color:"secondary.main"}; break;
  287. case "storing":
  288. switch (silStatus) {
  289. case "pending": btnSx = {label: t("process epqc"), color:"success.main"}; break;
  290. case "received": btnSx = {label: t("view putaway"), color:"secondary.main"}; break;
  291. case "escalated":
  292. if (sessionToken?.id == jo.silHandlerId) {
  293. btnSx = {label: t("escalation processing"), color:"warning.main"};
  294. break;
  295. }
  296. default: btnSx = {label: t("view stockin"), color:"info.main"};
  297. }
  298. break;
  299. case "completed": btnSx = {label: t("view stockin"), color:"info.main"}; break;
  300. default: btnSx = {label: t("scan picked material"), color:"success.main"};
  301. }
  302. return btnSx
  303. };
  304. const { data: session } = useSession();
  305. const sessionToken = session as SessionWithTokens | null;
  306. const [openModal, setOpenModal] = useState<boolean>(false);
  307. const [modalInfo, setModalInfo] = useState<StockInLineInput>();
  308. const onDetailClick = useCallback((record: JobOrder) => {
  309. if (record.status == "processing") {
  310. handleUpdate(record)
  311. } else if (record.status == "storing" || record.status == "completed") {
  312. if (record.stockInLineId != null) {
  313. const data = {
  314. id: record.stockInLineId,
  315. expiryDate: arrayToDateString(dayjs().add(1, "month"), "input"),
  316. }
  317. setModalInfo(data);
  318. setOpenModal(true);
  319. } else { alert('Invalid Stock In Line Id'); }
  320. } else {
  321. router.push(`/jo/edit?id=${record.id}`)
  322. }
  323. }, [])
  324. const closeNewModal = useCallback(() => {
  325. // const response = updateJo({ id: 1, status: "storing" });
  326. setOpenModal(false); // Close the modal first
  327. // setTimeout(() => {
  328. // }, 300); // Add a delay to avoid immediate re-trigger of useEffect
  329. refetchData(defaultInputs, "search");
  330. }, []);
  331. const onSearch = useCallback((query: Record<SearchParamNames, string>) => {
  332. setInputs(() => ({
  333. code: query.code,
  334. itemName: query.itemName,
  335. planStart: query.planStart,
  336. planStartTo: query.planStartTo
  337. }))
  338. refetchData(query, "search");
  339. }, [])
  340. const onReset = useCallback(() => {
  341. refetchData(defaultInputs, "paging");
  342. }, [])
  343. // Manual Create Jo Related
  344. const onOpenCreateJoModal = useCallback(() => {
  345. setIsCreateJoModalOpen(() => true)
  346. }, [])
  347. const onCloseCreateJoModal = useCallback(() => {
  348. setIsCreateJoModalOpen(() => false)
  349. }, [])
  350. return <>
  351. <Stack
  352. direction="row"
  353. justifyContent="flex-end"
  354. spacing={2}
  355. sx={{ mt: 2 }}
  356. >
  357. <Button
  358. variant="outlined"
  359. startIcon={<AddIcon />}
  360. onClick={onOpenCreateJoModal}
  361. >
  362. {t("Create Job Order")}
  363. </Button>
  364. </Stack>
  365. <SearchBox
  366. criteria={searchCriteria}
  367. onSearch={onSearch}
  368. onReset={onReset}
  369. />
  370. <SearchResults<JobOrder>
  371. items={filteredJos}
  372. columns={columns}
  373. setPagingController={setPagingController}
  374. pagingController={pagingController}
  375. totalCount={totalCount}
  376. // isAutoPaging={false}
  377. />
  378. <JoCreateFormModal
  379. open={isCreateJoModalOpen}
  380. bomCombo={bomCombo}
  381. onClose={onCloseCreateJoModal}
  382. onSearch={searchDataByPage}
  383. />
  384. <QcStockInModal
  385. session={sessionToken}
  386. open={openModal}
  387. onClose={closeNewModal}
  388. inputDetail={modalInfo}
  389. printerCombo={printerCombo}
  390. // skipQc={true}
  391. />
  392. </>
  393. }
  394. export default JoSearch;