FPSMS-frontend
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 

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