FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

930 regels
38 KiB

  1. "use client"
  2. import { SearchJoResultRequest, fetchJos, releaseJo, setJobOrderHidden, updateJo, updateProductProcessPriority, updateJoReqQty } 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, Dialog, DialogTitle, DialogContent, DialogActions, TextField, IconButton, InputAdornment, Typography, Box, CircularProgress } 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 EditIcon from '@mui/icons-material/Edit';
  20. import QcStockInModal from "../Qc/QcStockInModal";
  21. import { useSession } from "next-auth/react";
  22. import { SessionWithTokens } from "@/config/authConfig";
  23. import { createStockInLine } from "@/app/api/stockIn/actions";
  24. import { msg } from "../Swal/CustomAlerts";
  25. import dayjs from "dayjs";
  26. //import { fetchInventories } from "@/app/api/inventory/actions";
  27. import { InventoryResult } from "@/app/api/inventory";
  28. import { PrinterCombo } from "@/app/api/settings/printer";
  29. import { JobTypeResponse } from "@/app/api/jo/actions";
  30. import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
  31. import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
  32. import { updateJoPlanStart } from "@/app/api/jo/actions";
  33. import { arrayToDayjs } from "@/app/utils/formatUtil";
  34. import { useJoCreatePlanStartPrefs } from "@/hooks/useJoCreatePlanStartPrefs";
  35. interface Props {
  36. defaultInputs: SearchJoResultRequest,
  37. bomCombo: BomCombo[]
  38. printerCombo: PrinterCombo[];
  39. jobTypes: JobTypeResponse[];
  40. }
  41. type SearchParamNames = "code" | "itemName" | "planStart" | "planStartTo" | "jobTypeName" | "joSearchStatus";
  42. const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobTypes }) => {
  43. const { t } = useTranslation("jo");
  44. const router = useRouter()
  45. const [filteredJos, setFilteredJos] = useState<JobOrder[]>([]);
  46. const [inputs, setInputs] = useState(defaultInputs);
  47. const [pagingController, setPagingController] = useState(
  48. defaultPagingController
  49. )
  50. const [totalCount, setTotalCount] = useState(0)
  51. const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false)
  52. const {
  53. rememberPlanStart,
  54. defaultPlanStartForCreate,
  55. handleRememberPlanStartChange,
  56. } = useJoCreatePlanStartPrefs()
  57. const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]);
  58. const [detailedJos, setDetailedJos] = useState<Map<number, JobOrder>>(new Map());
  59. const [checkboxIds, setCheckboxIds] = useState<(string | number)[]>([]);
  60. const [releasingJoIds, setReleasingJoIds] = useState<Set<number>>(new Set());
  61. const [isBatchReleasing, setIsBatchReleasing] = useState(false);
  62. const [cancelConfirmJoId, setCancelConfirmJoId] = useState<number | null>(null);
  63. const [cancelSubmitting, setCancelSubmitting] = useState(false);
  64. const [cancelingJoIds, setCancelingJoIds] = useState<Set<number>>(new Set());
  65. // 合并后的统一编辑 Dialog 状态
  66. const [openEditDialog, setOpenEditDialog] = useState(false);
  67. const [selectedJoForEdit, setSelectedJoForEdit] = useState<JobOrder | null>(null);
  68. const [editPlanStartDate, setEditPlanStartDate] = useState<dayjs.Dayjs | null>(null);
  69. const [editReqQtyMultiplier, setEditReqQtyMultiplier] = useState<number>(1);
  70. const [editBomForReqQty, setEditBomForReqQty] = useState<BomCombo | null>(null);
  71. const [editProductionPriority, setEditProductionPriority] = useState<number>(50);
  72. const [editProductProcessId, setEditProductProcessId] = useState<number | null>(null);
  73. const fetchJoDetailClient = async (id: number): Promise<JobOrder> => {
  74. const response = await fetch(`/api/jo/detail?id=${id}`);
  75. if (!response.ok) {
  76. throw new Error('Failed to fetch JO detail');
  77. }
  78. return response.json();
  79. };
  80. /*
  81. useEffect(() => {
  82. const fetchDetailedJos = async () => {
  83. const detailedMap = new Map<number, JobOrder>();
  84. try {
  85. const results = await Promise.all(
  86. filteredJos.map((jo) =>
  87. fetchJoDetailClient(jo.id).then((detail) => ({ id: jo.id, detail })).catch((error) => {
  88. console.error(`Error fetching detail for JO ${jo.id}:`, error);
  89. return null;
  90. })
  91. )
  92. );
  93. results.forEach((r) => {
  94. if (r) detailedMap.set(r.id, r.detail);
  95. });
  96. } catch (error) {
  97. console.error("Error fetching JO details:", error);
  98. }
  99. setDetailedJos(detailedMap);
  100. };
  101. if (filteredJos.length > 0) {
  102. fetchDetailedJos();
  103. }
  104. }, [filteredJos]);
  105. */
  106. /*
  107. useEffect(() => {
  108. const fetchInventoryData = async () => {
  109. try {
  110. const inventoryResponse = await fetchInventories({
  111. code: "",
  112. name: "",
  113. type: "",
  114. pageNum: 0,
  115. pageSize: 200,
  116. });
  117. setInventoryData(inventoryResponse.records ?? []);
  118. } catch (error) {
  119. console.error("Error fetching inventory data:", error);
  120. }
  121. };
  122. fetchInventoryData();
  123. }, []);
  124. */
  125. const getStockAvailable = (pickLine: JoDetailPickLine) => {
  126. const inventory = inventoryData.find(inventory =>
  127. inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name
  128. );
  129. if (inventory) {
  130. return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty);
  131. }
  132. return 0;
  133. };
  134. const isStockSufficient = (pickLine: JoDetailPickLine) => {
  135. const stockAvailable = getStockAvailable(pickLine);
  136. return stockAvailable >= pickLine.reqQty;
  137. };
  138. const getStockCounts = (jo: JobOrder) => {
  139. return {
  140. sufficient: jo.sufficientCount,
  141. insufficient: jo.insufficientCount
  142. };
  143. };
  144. const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => [
  145. { label: t("Code"), paramName: "code", type: "text" },
  146. { label: t("Item Name"), paramName: "itemName", type: "text" },
  147. { label: t("Plan Start"), label2: t("Plan Start To"), paramName: "planStart", type: "dateRange", preFilledValue: {
  148. from: dayjsToDateString(dayjs(), "input"),
  149. to: dayjsToDateString(dayjs(), "input")
  150. } },
  151. {
  152. label: t("Job Type"),
  153. paramName: "jobTypeName",
  154. type: "select",
  155. options: jobTypes.map(jt => jt.name)
  156. },
  157. {
  158. label: t("Status"),
  159. paramName: "joSearchStatus",
  160. type: "select-labelled",
  161. options: [
  162. { label: t("Pending"), value: "pending" },
  163. { label: t("Packaging"), value: "packaging" },
  164. { label: t("Processing"), value: "processing" },
  165. { label: t("Storing"), value: "storing" },
  166. { label: t("Put Awayed"), value: "putAwayed" },
  167. { label: t("cancel"), value: "cancel" },
  168. ],
  169. },
  170. ], [t, jobTypes])
  171. const fetchBomForJo = useCallback(async (jo: JobOrder): Promise<BomCombo | null> => {
  172. try {
  173. // 若列表的 jo 已有 bomId(之後若 API 有回傳),可直接用
  174. const bomId = (jo as { bomId?: number }).bomId;
  175. if (bomId != null) {
  176. const matchingBom = bomCombo.find(bom => bom.id === bomId);
  177. if (matchingBom) return matchingBom;
  178. }
  179. // 否則打明細 API,明細會帶 bomId
  180. const detailedJo = detailedJos.get(jo.id) ?? await fetchJoDetailClient(jo.id);
  181. const detailBomId = (detailedJo as { bomId?: number }).bomId;
  182. if (detailBomId != null) {
  183. const matchingBom = bomCombo.find(bom => bom.id === detailBomId);
  184. if (matchingBom) return matchingBom;
  185. }
  186. return null;
  187. } catch (error) {
  188. console.error("Error fetching BOM for JO:", error);
  189. return null;
  190. }
  191. }, [bomCombo, detailedJos]);
  192. // 统一的打开编辑对话框函数
  193. const handleOpenEditDialog = useCallback(async (jo: JobOrder) => {
  194. setSelectedJoForEdit(jo);
  195. // 设置 Plan Start Date
  196. if (jo.planStart && Array.isArray(jo.planStart)) {
  197. setEditPlanStartDate(arrayToDayjs(jo.planStart));
  198. } else {
  199. setEditPlanStartDate(dayjs());
  200. }
  201. // 设置 Production Priority
  202. setEditProductionPriority(jo.productionPriority ?? 50);
  203. // 获取 productProcessId
  204. try {
  205. const { fetchProductProcessesByJobOrderId } = await import("@/app/api/jo/actions");
  206. const processes = await fetchProductProcessesByJobOrderId(jo.id);
  207. if (processes && processes.length > 0) {
  208. setEditProductProcessId(processes[0].id);
  209. }
  210. } catch (error) {
  211. console.error("Error fetching product process:", error);
  212. }
  213. // 设置 ReqQty
  214. const bom = await fetchBomForJo(jo);
  215. if (bom) {
  216. setEditBomForReqQty(bom);
  217. const currentMultiplier = bom.outputQty > 0
  218. ? Math.round(jo.reqQty / bom.outputQty)
  219. : 1;
  220. setEditReqQtyMultiplier(currentMultiplier);
  221. }
  222. setOpenEditDialog(true);
  223. }, [fetchBomForJo]);
  224. // 统一的关闭函数
  225. const handleCloseEditDialog = useCallback((_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => {
  226. setOpenEditDialog(false);
  227. setSelectedJoForEdit(null);
  228. setEditPlanStartDate(null);
  229. setEditReqQtyMultiplier(1);
  230. setEditBomForReqQty(null);
  231. setEditProductionPriority(50);
  232. setEditProductProcessId(null);
  233. }, []);
  234. const newPageFetch = useCallback(
  235. async (
  236. pagingController: { pageNum: number; pageSize: number },
  237. filterArgs: SearchJoResultRequest,
  238. ) => {
  239. const params: SearchJoResultRequest = {
  240. ...filterArgs,
  241. pageNum: pagingController.pageNum - 1,
  242. pageSize: pagingController.pageSize,
  243. };
  244. const response = await fetchJos(params);
  245. console.log("newPageFetch params:", params)
  246. console.log("newPageFetch response:", response)
  247. if (response && response.records) {
  248. console.log("newPageFetch - setting filteredJos with", response.records.length, "records");
  249. setTotalCount(response.total);
  250. setFilteredJos(response.records);
  251. console.log("newPageFetch - filteredJos set, first record id:", response.records[0]?.id);
  252. } else {
  253. console.warn("newPageFetch - no response or no records");
  254. setFilteredJos([]);
  255. }
  256. },
  257. [],
  258. );
  259. const isPlanningJo = useCallback((jo: JobOrder) => {
  260. return String(jo.status ?? "").toLowerCase() === "planning";
  261. }, []);
  262. const handleReleaseJo = useCallback(async (joId: number) => {
  263. if (!joId) return;
  264. setReleasingJoIds((prev) => {
  265. const next = new Set(prev);
  266. next.add(joId);
  267. return next;
  268. });
  269. try {
  270. const response = await releaseJo({ id: joId });
  271. if (response) {
  272. msg(t("update success"));
  273. setCheckboxIds((prev) => prev.filter((id) => Number(id) !== joId));
  274. await newPageFetch(pagingController, inputs);
  275. }
  276. } catch (error) {
  277. console.error("Error releasing JO:", error);
  278. msg(t("update failed"));
  279. } finally {
  280. setReleasingJoIds((prev) => {
  281. const next = new Set(prev);
  282. next.delete(joId);
  283. return next;
  284. });
  285. }
  286. }, [inputs, pagingController, t, newPageFetch]);
  287. const handleConfirmCancelJobOrder = useCallback(async () => {
  288. if (cancelConfirmJoId == null) return;
  289. const id = cancelConfirmJoId;
  290. setCancelSubmitting(true);
  291. setCancelingJoIds((prev) => new Set(prev).add(id));
  292. try {
  293. await setJobOrderHidden(id, true);
  294. msg(t("update success"));
  295. setCancelConfirmJoId(null);
  296. await newPageFetch(pagingController, inputs);
  297. } catch (error) {
  298. console.error("Error cancelling job order:", error);
  299. msg(t("update failed"));
  300. } finally {
  301. setCancelSubmitting(false);
  302. setCancelingJoIds((prev) => {
  303. const next = new Set(prev);
  304. next.delete(id);
  305. return next;
  306. });
  307. }
  308. }, [cancelConfirmJoId, newPageFetch, pagingController, inputs, t]);
  309. const selectedPlanningJoIds = useMemo(() => {
  310. const selectedIds = new Set(checkboxIds.map((id) => Number(id)));
  311. return filteredJos
  312. .filter((jo) => selectedIds.has(jo.id))
  313. .filter((jo) => isPlanningJo(jo))
  314. .map((jo) => jo.id);
  315. }, [checkboxIds, filteredJos, isPlanningJo]);
  316. const handleBatchRelease = useCallback(async () => {
  317. if (selectedPlanningJoIds.length === 0) return;
  318. setIsBatchReleasing(true);
  319. try {
  320. const results = await Promise.allSettled(
  321. selectedPlanningJoIds.map((id) => releaseJo({ id })),
  322. );
  323. const successCount = results.filter((r) => r.status === "fulfilled").length;
  324. const failedCount = results.length - successCount;
  325. if (successCount > 0 && failedCount === 0) {
  326. msg(t("update success"));
  327. } else if (successCount > 0) {
  328. msg(`${t("update success")} (${successCount}), ${t("update failed")} (${failedCount})`);
  329. } else {
  330. msg(t("update failed"));
  331. }
  332. setCheckboxIds((prev) => prev.filter((id) => !selectedPlanningJoIds.includes(Number(id))));
  333. await newPageFetch(pagingController, inputs);
  334. } catch (error) {
  335. console.error("Error batch releasing JOs:", error);
  336. msg(t("update failed"));
  337. } finally {
  338. setIsBatchReleasing(false);
  339. }
  340. }, [inputs, pagingController, selectedPlanningJoIds, t, newPageFetch]);
  341. const columns = useMemo<Column<JobOrder>[]>(
  342. () => [
  343. {
  344. name: "id",
  345. label: "",
  346. type: "checkbox",
  347. disabled: (row) => {
  348. const id = row.id;
  349. return !isPlanningJo(row) || releasingJoIds.has(id) || isBatchReleasing;
  350. }
  351. },
  352. {
  353. name: "planStart",
  354. label: t("Estimated Production Date"),
  355. align: "left",
  356. headerAlign: "left",
  357. renderCell: (row) => {
  358. return (
  359. <Stack direction="row" alignItems="center" spacing={1}>
  360. {row.status == "planning" && (
  361. <IconButton
  362. size="small"
  363. onClick={(e) => {
  364. e.stopPropagation();
  365. handleOpenEditDialog(row);
  366. }}
  367. sx={{ padding: '4px' }}
  368. >
  369. <EditIcon fontSize="small" />
  370. </IconButton>
  371. )}
  372. <span>{row.planStart ? arrayToDateString(row.planStart) : '-'}</span>
  373. </Stack>
  374. );
  375. }
  376. },
  377. {
  378. name: "productionPriority",
  379. label: t("Production Priority"),
  380. renderCell: (row) => {
  381. return (
  382. <Stack direction="row" alignItems="center" spacing={1}>
  383. <span>{integerFormatter.format(row.productionPriority)}</span>
  384. </Stack>
  385. );
  386. }
  387. },
  388. {
  389. name: "code",
  390. label: t("Code / Lot No"),
  391. flex: 2,
  392. renderCell: (row) => (
  393. <span>
  394. {row.code}
  395. <br />
  396. {row.lotNo ?? "-"}
  397. </span>
  398. ),
  399. },
  400. {
  401. name: "item",
  402. label: `${t("Item Name")}`,
  403. renderCell: (row) => {
  404. return row.item ? <>{t(row.jobTypeName)} {t(row.item.code)} {t(row.item.name)}</> : '-'
  405. }
  406. },
  407. {
  408. name: "reqQty",
  409. label: t("Req. Qty"),
  410. align: "right",
  411. headerAlign: "right",
  412. renderCell: (row) => {
  413. return (
  414. <Stack direction="row" alignItems="center" spacing={1} justifyContent="flex-end">
  415. <span>{integerFormatter.format(row.reqQty)}</span>
  416. </Stack>
  417. );
  418. }
  419. },
  420. {
  421. name: "item",
  422. label: t("UoM"),
  423. align: "left",
  424. headerAlign: "left",
  425. renderCell: (row) => {
  426. return row.item?.uom ? t(row.item.uom.udfudesc) : '-'
  427. }
  428. },
  429. {
  430. name: "stockStatus" as keyof JobOrder,
  431. label: t("BOM Status"),
  432. align: "left",
  433. headerAlign: "left",
  434. renderCell: (row) => {
  435. const stockCounts = getStockCounts(row);
  436. return (
  437. <span style={{ color: stockCounts.insufficient > 0 ? 'red' : 'green' }}>
  438. {stockCounts.sufficient}/{stockCounts.sufficient + stockCounts.insufficient}
  439. </span>
  440. );
  441. }
  442. },
  443. {
  444. name: "status",
  445. label: t("Status"),
  446. renderCell: (row) => {
  447. return <span style={{color: row.stockInLineStatus == "escalated" ? "red" : "inherit"}}>
  448. {t(upperFirst(row.status))}
  449. </span>
  450. }
  451. },
  452. {
  453. name: "id",
  454. label: t("Actions"),
  455. renderCell: (row) => {
  456. const isPlanning = isPlanningJo(row);
  457. const isReleasing = releasingJoIds.has(row.id) || isBatchReleasing;
  458. const isCancelingRow = cancelingJoIds.has(row.id);
  459. const isPutAwayed = row.stockInLineStatus?.toLowerCase() === "completed";
  460. return (
  461. <Stack direction="row" spacing={1} alignItems="center">
  462. <Button
  463. type="button"
  464. variant="contained"
  465. color="primary"
  466. sx={{ minWidth: 120 }}
  467. onClick={(e) => {
  468. e.stopPropagation();
  469. onDetailClick(row);
  470. }}
  471. >
  472. {t("View")}
  473. </Button>
  474. {isPlanning ? (
  475. <Button
  476. type="button"
  477. variant="contained"
  478. color="success"
  479. disabled={!isPlanning || isReleasing}
  480. sx={{ minWidth: 120 }}
  481. onClick={(e) => {
  482. e.stopPropagation();
  483. handleReleaseJo(row.id);
  484. }}
  485. startIcon={isReleasing && isPlanning ? <CircularProgress size={16} color="inherit" /> : undefined}
  486. >
  487. {t("Release")}
  488. </Button>
  489. ) : (
  490. <Button
  491. type="button"
  492. variant="contained"
  493. color="warning"
  494. disabled={isPutAwayed || isCancelingRow || isBatchReleasing || cancelSubmitting}
  495. sx={{ minWidth: 120 }}
  496. onClick={(e) => {
  497. e.stopPropagation();
  498. setCancelConfirmJoId(row.id);
  499. }}
  500. startIcon={isCancelingRow ? <CircularProgress size={16} color="inherit" /> : undefined}
  501. >
  502. {t("Cancel Job Order")}
  503. </Button>
  504. )}
  505. </Stack>
  506. )
  507. }
  508. },
  509. ], [t, inventoryData, detailedJos, handleOpenEditDialog, handleReleaseJo, isPlanningJo, releasingJoIds, isBatchReleasing, cancelingJoIds, cancelSubmitting]
  510. )
  511. const handleUpdateReqQty = useCallback(async (jobOrderId: number, newReqQty: number) => {
  512. try {
  513. const response = await updateJoReqQty({
  514. id: jobOrderId,
  515. reqQty: newReqQty
  516. });
  517. if (response) {
  518. msg(t("update success"));
  519. await newPageFetch(pagingController, inputs);
  520. }
  521. } catch (error) {
  522. console.error("Error updating reqQty:", error);
  523. msg(t("update failed"));
  524. }
  525. }, [pagingController, inputs, newPageFetch, t]);
  526. const handleUpdatePlanStart = useCallback(async (jobOrderId: number, planStart: string) => {
  527. const response = await updateJoPlanStart({ id: jobOrderId, planStart });
  528. if (response) {
  529. await newPageFetch(pagingController, inputs);
  530. }
  531. }, [pagingController, inputs, newPageFetch]);
  532. const handleUpdateOperationPriority = useCallback(async (productProcessId: number, productionPriority: number) => {
  533. const response = await updateProductProcessPriority(productProcessId, productionPriority)
  534. if (response) {
  535. await newPageFetch(pagingController, inputs);
  536. }
  537. }, [pagingController, inputs, newPageFetch]);
  538. // 统一的确认函数
  539. const handleConfirmEdit = useCallback(async () => {
  540. if (!selectedJoForEdit) return;
  541. try {
  542. // 更新 Plan Start
  543. if (editPlanStartDate) {
  544. const dateString = `${dayjsToDateString(editPlanStartDate, "input")}T00:00:00`;
  545. await handleUpdatePlanStart(selectedJoForEdit.id, dateString);
  546. }
  547. // 更新 ReqQty
  548. if (editBomForReqQty) {
  549. const newReqQty = editReqQtyMultiplier * editBomForReqQty.outputQty;
  550. await handleUpdateReqQty(selectedJoForEdit.id, newReqQty);
  551. }
  552. // 更新 Production Priority
  553. if (editProductProcessId) {
  554. await handleUpdateOperationPriority(editProductProcessId, Number(editProductionPriority));
  555. }
  556. setOpenEditDialog(false);
  557. setSelectedJoForEdit(null);
  558. setEditPlanStartDate(null);
  559. setEditReqQtyMultiplier(1);
  560. setEditBomForReqQty(null);
  561. setEditProductionPriority(50);
  562. setEditProductProcessId(null);
  563. } catch (error) {
  564. console.error("Error updating:", error);
  565. msg(t("update failed"));
  566. }
  567. }, [selectedJoForEdit, editPlanStartDate, editBomForReqQty, editReqQtyMultiplier, editProductionPriority, editProductProcessId, handleUpdatePlanStart, handleUpdateReqQty, handleUpdateOperationPriority, t]);
  568. useEffect(() => {
  569. newPageFetch(pagingController, inputs);
  570. }, [newPageFetch, pagingController, inputs]);
  571. const handleUpdate = useCallback(async (jo: JobOrder) => {
  572. console.log(jo);
  573. try {
  574. if (jo.id) {
  575. const response = await updateJo({ id: jo.id, status: "storing" });
  576. console.log(`%c Updated JO:`, "color:lime", response);
  577. const postData = {
  578. itemId: jo?.item?.id!!,
  579. acceptedQty: jo?.reqQty ?? 1,
  580. productLotNo: jo?.code,
  581. productionDate: arrayToDateString(dayjs(), "input"),
  582. jobOrderId: jo?.id,
  583. };
  584. const res = await createStockInLine(postData);
  585. console.log(`%c Created Stock In Line`, "color:lime", res);
  586. msg(t("update success"));
  587. setInputs(defaultInputs);
  588. setPagingController(defaultPagingController);
  589. }
  590. } catch (e) {
  591. console.log(e);
  592. } finally {
  593. // setIsUploading(false)
  594. }
  595. }, [defaultInputs, t])
  596. const getButtonSx = (jo : JobOrder) => {
  597. const joStatus = jo.status?.toLowerCase();
  598. const silStatus = jo.stockInLineStatus?.toLowerCase();
  599. let btnSx = {label:"", color:""};
  600. switch (joStatus) {
  601. case "planning": btnSx = {label: t("release jo"), color:"primary.main"}; break;
  602. case "pending": btnSx = {label: t("scan picked material"), color:"error.main"}; break;
  603. case "processing": btnSx = {label: t("complete jo"), color:"warning.main"}; break;
  604. case "storing":
  605. switch (silStatus) {
  606. case "pending": btnSx = {label: t("process epqc"), color:"success.main"}; break;
  607. case "received": btnSx = {label: t("view putaway"), color:"secondary.main"}; break;
  608. case "escalated":
  609. if (sessionToken?.id == jo.silHandlerId) {
  610. btnSx = {label: t("escalation processing"), color:"warning.main"};
  611. break;
  612. }
  613. default: btnSx = {label: t("view stockin"), color:"info.main"};
  614. }
  615. break;
  616. case "completed": btnSx = {label: t("view stockin"), color:"info.main"}; break;
  617. default: btnSx = {label: t("scan picked material"), color:"success.main"};
  618. }
  619. return btnSx
  620. };
  621. const { data: session } = useSession();
  622. const sessionToken = session as SessionWithTokens | null;
  623. const [openModal, setOpenModal] = useState<boolean>(false);
  624. const [modalInfo, setModalInfo] = useState<StockInLineInput>();
  625. const onDetailClick = useCallback((record: JobOrder) => {
  626. router.push(`/jo/edit?id=${record.id}`)
  627. }, [router])
  628. const closeNewModal = useCallback(() => {
  629. setOpenModal(false);
  630. setInputs(defaultInputs);
  631. setPagingController(defaultPagingController);
  632. }, [defaultInputs]);
  633. const onSearch = useCallback((query: Record<SearchParamNames, string>) => {
  634. const transformedQuery = {
  635. ...query,
  636. planStart: query.planStart ? `${query.planStart}T00:00` : query.planStart,
  637. planStartTo: query.planStartTo ? `${query.planStartTo}T23:59:59` : query.planStartTo,
  638. jobTypeName: query.jobTypeName && query.jobTypeName !== "All" ? query.jobTypeName : "",
  639. joSearchStatus: query.joSearchStatus && query.joSearchStatus !== "All" ? query.joSearchStatus : "all",
  640. };
  641. setInputs({
  642. code: transformedQuery.code,
  643. itemName: transformedQuery.itemName,
  644. planStart: transformedQuery.planStart,
  645. planStartTo: transformedQuery.planStartTo,
  646. jobTypeName: transformedQuery.jobTypeName,
  647. joSearchStatus: transformedQuery.joSearchStatus
  648. });
  649. setPagingController(defaultPagingController);
  650. }, [defaultInputs])
  651. const onReset = useCallback(() => {
  652. setInputs(defaultInputs);
  653. setPagingController(defaultPagingController);
  654. }, [defaultInputs])
  655. const onOpenCreateJoModal = useCallback(() => {
  656. setIsCreateJoModalOpen(() => true)
  657. }, [])
  658. const onCloseCreateJoModal = useCallback(() => {
  659. setIsCreateJoModalOpen(() => false)
  660. }, [])
  661. return <>
  662. <Stack
  663. direction="row"
  664. justifyContent="flex-end"
  665. spacing={2}
  666. sx={{ mt: 2 }}
  667. >
  668. <Stack direction="row" alignItems="center" spacing={1} sx={{ mr: "auto" }}>
  669. <Typography variant="body2" color="text.secondary">
  670. {t("Selected")}: {selectedPlanningJoIds.length}
  671. </Typography>
  672. <Button
  673. variant="contained"
  674. color="success"
  675. disabled={selectedPlanningJoIds.length === 0 || isBatchReleasing}
  676. onClick={handleBatchRelease}
  677. startIcon={isBatchReleasing ? <CircularProgress size={16} color="inherit" /> : undefined}
  678. >
  679. {t("Release")} ({selectedPlanningJoIds.length})
  680. </Button>
  681. <Button
  682. variant="outlined"
  683. disabled={checkboxIds.length === 0 || isBatchReleasing}
  684. onClick={() => setCheckboxIds([])}
  685. >
  686. {t("Reset")}
  687. </Button>
  688. </Stack>
  689. <Button
  690. variant="outlined"
  691. startIcon={<AddIcon />}
  692. onClick={onOpenCreateJoModal}
  693. >
  694. {t("Create Job Order")}
  695. </Button>
  696. </Stack>
  697. <SearchBox
  698. criteria={searchCriteria}
  699. onSearch={onSearch}
  700. onReset={onReset}
  701. />
  702. <SearchResults<JobOrder>
  703. items={filteredJos}
  704. columns={columns}
  705. setPagingController={setPagingController}
  706. pagingController={pagingController}
  707. totalCount={totalCount}
  708. isAutoPaging={false}
  709. checkboxIds={checkboxIds}
  710. setCheckboxIds={setCheckboxIds}
  711. />
  712. <JoCreateFormModal
  713. open={isCreateJoModalOpen}
  714. bomCombo={bomCombo}
  715. jobTypes={jobTypes}
  716. defaultPlanStart={defaultPlanStartForCreate}
  717. rememberPlanStart={rememberPlanStart}
  718. onRememberPlanStartChange={handleRememberPlanStartChange}
  719. onClose={onCloseCreateJoModal}
  720. onSearch={() => {
  721. setInputs({ ...defaultInputs });
  722. setPagingController(defaultPagingController);
  723. }}
  724. />
  725. <QcStockInModal
  726. session={sessionToken}
  727. open={openModal}
  728. onClose={closeNewModal}
  729. inputDetail={modalInfo}
  730. printerCombo={printerCombo}
  731. />
  732. {/* 合并后的统一编辑 Dialog */}
  733. <Dialog
  734. open={openEditDialog}
  735. onClose={handleCloseEditDialog}
  736. fullWidth
  737. maxWidth="sm"
  738. >
  739. <DialogTitle>{t("Edit Job Order")}</DialogTitle>
  740. <DialogContent>
  741. <Stack spacing={3} sx={{ mt: 1 }}>
  742. {/* Plan Start Date */}
  743. <LocalizationProvider dateAdapter={AdapterDayjs}>
  744. <DatePicker
  745. label={t("Estimated Production Date")}
  746. value={editPlanStartDate}
  747. onChange={(newValue) => setEditPlanStartDate(newValue)}
  748. slotProps={{
  749. textField: {
  750. fullWidth: true,
  751. margin: "dense",
  752. }
  753. }}
  754. />
  755. </LocalizationProvider>
  756. {/* Production Priority */}
  757. <TextField
  758. label={t("Production Priority")}
  759. type="number"
  760. fullWidth
  761. value={editProductionPriority}
  762. onChange={(e) => {
  763. const val = Number(e.target.value);
  764. if (val >= 1 && val <= 100) {
  765. setEditProductionPriority(val);
  766. }
  767. }}
  768. inputProps={{
  769. min: 1,
  770. max: 100,
  771. step: 1
  772. }}
  773. />
  774. {/* ReqQty */}
  775. {editBomForReqQty && (
  776. <Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
  777. <TextField
  778. label={t("Base Qty")}
  779. fullWidth
  780. type="number"
  781. variant="outlined"
  782. value={editBomForReqQty.outputQty}
  783. disabled
  784. InputProps={{
  785. endAdornment: editBomForReqQty.outputQtyUom ? (
  786. <InputAdornment position="end">
  787. <Typography variant="body2" sx={{ color: "text.secondary" }}>
  788. {editBomForReqQty.outputQtyUom}
  789. </Typography>
  790. </InputAdornment>
  791. ) : null
  792. }}
  793. sx={{ flex: 1 }}
  794. />
  795. <Typography variant="body1" sx={{ color: "text.secondary" }}>
  796. ×
  797. </Typography>
  798. <TextField
  799. label={t("Batch Count")}
  800. fullWidth
  801. type="number"
  802. variant="outlined"
  803. value={editReqQtyMultiplier}
  804. onChange={(e) => {
  805. const val = e.target.value === "" ? 1 : Math.max(1, Math.floor(Number(e.target.value)));
  806. setEditReqQtyMultiplier(val);
  807. }}
  808. inputProps={{
  809. min: 1,
  810. step: 1
  811. }}
  812. sx={{ flex: 1 }}
  813. />
  814. <Typography variant="body1" sx={{ color: "text.secondary" }}>
  815. =
  816. </Typography>
  817. <TextField
  818. label={t("Req. Qty")}
  819. fullWidth
  820. variant="outlined"
  821. type="number"
  822. value={editBomForReqQty ? (editReqQtyMultiplier * editBomForReqQty.outputQty) : ""}
  823. disabled
  824. InputProps={{
  825. endAdornment: editBomForReqQty.outputQtyUom ? (
  826. <InputAdornment position="end">
  827. <Typography variant="body2" sx={{ color: "text.secondary" }}>
  828. {editBomForReqQty.outputQtyUom}
  829. </Typography>
  830. </InputAdornment>
  831. ) : null
  832. }}
  833. sx={{ flex: 1 }}
  834. />
  835. </Box>
  836. )}
  837. </Stack>
  838. </DialogContent>
  839. <DialogActions>
  840. <Button onClick={handleCloseEditDialog}>{t("Cancel")}</Button>
  841. <Button
  842. variant="contained"
  843. onClick={handleConfirmEdit}
  844. disabled={!editPlanStartDate || !editBomForReqQty}
  845. >
  846. {t("Save")}
  847. </Button>
  848. </DialogActions>
  849. </Dialog>
  850. <Dialog
  851. open={cancelConfirmJoId !== null}
  852. onClose={() => !cancelSubmitting && setCancelConfirmJoId(null)}
  853. maxWidth="xs"
  854. fullWidth
  855. >
  856. <DialogTitle>{t("Confirm cancel job order")}</DialogTitle>
  857. <DialogContent>
  858. <Typography variant="body2">{t("Cancel job order confirm message")}</Typography>
  859. </DialogContent>
  860. <DialogActions>
  861. <Button onClick={() => setCancelConfirmJoId(null)} disabled={cancelSubmitting}>{t("Cancel")}</Button>
  862. <Button variant="contained" color="warning" onClick={() => void handleConfirmCancelJobOrder()} disabled={cancelSubmitting}>
  863. {cancelSubmitting ? <CircularProgress size={20} /> : t("Cancel Job Order")}
  864. </Button>
  865. </DialogActions>
  866. </Dialog>
  867. </>
  868. }
  869. export default JoSearch;