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.
 
 

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