FPSMS-frontend
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

JoSearch.tsx 38 KiB

4ヶ月前
7ヶ月前
9ヶ月前
8ヶ月前
7ヶ月前
8ヶ月前
8ヶ月前
4ヶ月前
6ヶ月前
5ヶ月前
5ヶ月前
7ヶ月前
8ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
4ヶ月前
4ヶ月前
7ヶ月前
4ヶ月前
6ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
10ヶ月前
6ヶ月前
6ヶ月前
10ヶ月前
7ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
6ヶ月前
7ヶ月前
6ヶ月前
10ヶ月前
8ヶ月前
8ヶ月前
4ヶ月前
4ヶ月前
6ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930
  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;