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.

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