FPSMS-frontend
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 

674 строки
24 KiB

  1. "use client";
  2. import React, { useCallback, useEffect, useMemo, useState } from "react";
  3. import {
  4. Box,
  5. Button,
  6. Card,
  7. CardContent,
  8. CardActions,
  9. Stack,
  10. Typography,
  11. Chip,
  12. CircularProgress,
  13. TablePagination,
  14. Grid,
  15. FormControl,
  16. InputLabel,
  17. Select,
  18. MenuItem,
  19. Checkbox,
  20. ListItemText,
  21. SelectChangeEvent,
  22. Dialog,
  23. DialogTitle,
  24. DialogContent,
  25. DialogActions,
  26. } from "@mui/material";
  27. import { useTranslation } from "react-i18next";
  28. import { fetchItemForPutAway } from "@/app/api/stockIn/actions";
  29. import QcStockInModal from "../Qc/QcStockInModal";
  30. import { useSession } from "next-auth/react";
  31. import { SessionWithTokens } from "@/config/authConfig";
  32. import dayjs from "dayjs";
  33. import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  34. import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox";
  35. import { AUTH } from "@/authorities";
  36. import {
  37. AllJoborderProductProcessInfoResponse,
  38. updateJo,
  39. fetchProductProcessesByJobOrderId,
  40. completeProductProcessLine,
  41. assignJobOrderPickOrder,
  42. fetchJoborderProductProcessesPage
  43. } from "@/app/api/jo/actions";
  44. import { StockInLineInput } from "@/app/api/stockIn";
  45. import { PrinterCombo } from "@/app/api/settings/printer";
  46. import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan";
  47. export type ProductionProcessListPersistedState = {
  48. date: string;
  49. itemCode: string | null;
  50. jobOrderCode: string | null;
  51. filter: "all" | "drink" | "other";
  52. page: number;
  53. selectedItemCodes: string[];
  54. };
  55. interface ProductProcessListProps {
  56. onSelectProcess: (jobOrderId: number|undefined, productProcessId: number|undefined) => void;
  57. onSelectMatchingStock: (jobOrderId: number|undefined, productProcessId: number|undefined,pickOrderId: number|undefined) => void;
  58. printerCombo: PrinterCombo[];
  59. qcReady: boolean;
  60. listPersistedState: ProductionProcessListPersistedState;
  61. onListPersistedStateChange: React.Dispatch<
  62. React.SetStateAction<ProductionProcessListPersistedState>
  63. >;
  64. }
  65. export type SearchParam = "date" | "itemCode" | "jobOrderCode" | "processType";
  66. const PAGE_SIZE = 50;
  67. /** 預設依 JobOrder.planStart 搜尋:今天往前 3 天~往後 3 天(含當日) */
  68. function defaultPlanStartRange() {
  69. return {
  70. from: dayjs().subtract(0, "day").format("YYYY-MM-DD"),
  71. to: dayjs().add(0, "day").format("YYYY-MM-DD"),
  72. };
  73. }
  74. export function createDefaultProductionProcessListPersistedState(): ProductionProcessListPersistedState {
  75. return {
  76. date: dayjs().format("YYYY-MM-DD"),
  77. itemCode: null,
  78. jobOrderCode: null,
  79. filter: "all",
  80. page: 0,
  81. selectedItemCodes: [],
  82. };
  83. }
  84. const ProductProcessList: React.FC<ProductProcessListProps> = ({
  85. onSelectProcess,
  86. printerCombo,
  87. onSelectMatchingStock,
  88. qcReady,
  89. listPersistedState,
  90. onListPersistedStateChange,
  91. }) => {
  92. const { t } = useTranslation( ["common", "production","purchaseOrder","dashboard"]);
  93. const { data: session } = useSession() as { data: SessionWithTokens | null };
  94. const sessionToken = session as SessionWithTokens | null;
  95. const [loading, setLoading] = useState(false);
  96. const [processes, setProcesses] = useState<AllJoborderProductProcessInfoResponse[]>([]);
  97. const [openModal, setOpenModal] = useState<boolean>(false);
  98. const [modalInfo, setModalInfo] = useState<StockInLineInput>();
  99. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  100. const abilities = session?.abilities ?? session?.user?.abilities ?? [];
  101. // 依照 DB `authority.authority = 'ADMIN'` 的逻辑:僅 abilities 明確包含 ADMIN 才能操作
  102. const canManageUpdateJo = abilities.some((a) => a.trim() === AUTH.ADMIN);
  103. type ProcessFilter = "all" | "drink" | "other";
  104. const [suggestedLocationCode, setSuggestedLocationCode] = useState<string | null>(null);
  105. const appliedSearch = useMemo(
  106. () => ({
  107. date: listPersistedState.date,
  108. itemCode: listPersistedState.itemCode,
  109. jobOrderCode: listPersistedState.jobOrderCode,
  110. }),
  111. [
  112. listPersistedState.date,
  113. listPersistedState.itemCode,
  114. listPersistedState.jobOrderCode,
  115. ],
  116. );
  117. const filter = listPersistedState.filter;
  118. const page = listPersistedState.page;
  119. const selectedItemCodes = listPersistedState.selectedItemCodes;
  120. const [totalJobOrders, setTotalJobOrders] = useState(0);
  121. // Generic confirm dialog for actions (update job order / etc.)
  122. const [confirmOpen, setConfirmOpen] = useState(false);
  123. const [confirmMessage, setConfirmMessage] = useState("");
  124. const [confirmLoading, setConfirmLoading] = useState(false);
  125. const [pendingConfirmAction, setPendingConfirmAction] = useState<null | (() => Promise<void>)>(null);
  126. // QC 的业务判定:同一个 jobOrder 下,所有 productProcess 的所有 lines 都必须是 Completed/Pass
  127. // 才允许打开 QcStockInModal(避免仅某个 productProcess 完成就提前出现 view stockin)。
  128. const jobOrderQcReadyById = useMemo(() => {
  129. const lineDone = (status: unknown) => {
  130. const s = String(status ?? "").trim().toLowerCase();
  131. return s === "completed" || s === "pass";
  132. };
  133. const byJobOrder = new Map<number, AllJoborderProductProcessInfoResponse[]>();
  134. for (const p of processes) {
  135. if (p.jobOrderId == null) continue;
  136. const arr = byJobOrder.get(p.jobOrderId) ?? [];
  137. arr.push(p);
  138. byJobOrder.set(p.jobOrderId, arr);
  139. }
  140. const result = new Map<number, boolean>();
  141. byJobOrder.forEach((jobOrderProcesses, jobOrderId) => {
  142. const hasStockInLine = jobOrderProcesses.some((p) => p.stockInLineId != null);
  143. const allLinesDone =
  144. jobOrderProcesses.length > 0 &&
  145. jobOrderProcesses.every((p) => {
  146. const lines = p.lines ?? [];
  147. // 没有 lines 的情况认为未完成,避免误放行
  148. return lines.length > 0 && lines.every((l) => lineDone(l.status));
  149. });
  150. result.set(jobOrderId, hasStockInLine && allLinesDone);
  151. });
  152. return result;
  153. }, [processes]);
  154. const handleAssignPickOrder = useCallback(async (pickOrderId: number, jobOrderId?: number, productProcessId?: number) => {
  155. if (!currentUserId) {
  156. alert(t("Unable to get user ID"));
  157. return;
  158. }
  159. try {
  160. console.log("🔄 Assigning pick order:", pickOrderId, "to user:", currentUserId);
  161. // 调用分配 API 并读取响应
  162. const assignResult = await assignJobOrderPickOrder(pickOrderId, currentUserId);
  163. console.log("📦 Assign result:", assignResult);
  164. // 检查分配是否成功
  165. if (assignResult.message === "Successfully assigned") {
  166. console.log("✅ Successfully assigned pick order");
  167. console.log("✅ Pick order ID:", assignResult.id);
  168. console.log("✅ Pick order code:", assignResult.code);
  169. // 分配成功后,导航到 second scan 页面
  170. if (onSelectMatchingStock && jobOrderId) {
  171. onSelectMatchingStock(jobOrderId, productProcessId,pickOrderId);
  172. } else {
  173. alert(t("Assignment successful"));
  174. }
  175. } else {
  176. // 分配失败
  177. console.error("Assignment failed:", assignResult.message);
  178. alert(t(`Assignment failed: ${assignResult.message || "Unknown error"}`));
  179. }
  180. } catch (error: any) {
  181. console.error(" Error assigning pick order:", error);
  182. alert(t(`Unknown error: ${error?.message || "Unknown error"}。Please try again later.`));
  183. }
  184. }, [currentUserId, t, onSelectMatchingStock]);
  185. const handleViewStockIn = useCallback((process: AllJoborderProductProcessInfoResponse) => {
  186. if (!process.stockInLineId) {
  187. alert(t("Invalid Stock In Line Id"));
  188. return;
  189. }
  190. setModalInfo({
  191. id: process.stockInLineId,
  192. //itemId: process.itemId, // 如果 process 中有 itemId,添加这一行
  193. //expiryDate: dayjs().add(1, "month").format(OUTPUT_DATE_FORMAT),
  194. });
  195. setOpenModal(true);
  196. }, [t]);
  197. const handleApplySearch = useCallback(
  198. (inputs: Record<SearchParam | `${SearchParam}To`, string>) => {
  199. const selectedProcessType = (inputs.processType || "all") as ProcessFilter;
  200. const fallback = defaultPlanStartRange();
  201. const selectedDate = (inputs.date || "").trim() || fallback.from;
  202. onListPersistedStateChange((prev) => ({
  203. ...prev,
  204. filter: selectedProcessType,
  205. date: selectedDate,
  206. itemCode: inputs.itemCode?.trim() ? inputs.itemCode.trim() : null,
  207. jobOrderCode: inputs.jobOrderCode?.trim() ? inputs.jobOrderCode.trim() : null,
  208. selectedItemCodes: [],
  209. page: 0,
  210. }));
  211. },
  212. [onListPersistedStateChange],
  213. );
  214. const handleResetSearch = useCallback(() => {
  215. const r = defaultPlanStartRange();
  216. onListPersistedStateChange((prev) => ({
  217. ...prev,
  218. filter: "all",
  219. date: r.from,
  220. itemCode: null,
  221. jobOrderCode: null,
  222. selectedItemCodes: [],
  223. page: 0,
  224. }));
  225. }, [onListPersistedStateChange]);
  226. const fetchProcesses = useCallback(async () => {
  227. setLoading(true);
  228. try {
  229. const isDrinkParam =
  230. filter === "all" ? undefined : filter === "drink" ? true : false;
  231. const data = await fetchJoborderProductProcessesPage({
  232. date: appliedSearch.date,
  233. itemCode: appliedSearch.itemCode,
  234. jobOrderCode: appliedSearch.jobOrderCode,
  235. qcReady,
  236. isDrink: isDrinkParam,
  237. page,
  238. size: PAGE_SIZE,
  239. });
  240. setProcesses(data?.content || []);
  241. setTotalJobOrders(data?.totalJobOrders || 0);
  242. } catch (e) {
  243. console.error(e);
  244. setProcesses([]);
  245. setTotalJobOrders(0);
  246. } finally {
  247. setLoading(false);
  248. }
  249. }, [listPersistedState, qcReady]);
  250. useEffect(() => {
  251. fetchProcesses();
  252. }, [fetchProcesses]);
  253. const handleUpdateJo = useCallback(async (process: AllJoborderProductProcessInfoResponse) => {
  254. if (!canManageUpdateJo) return;
  255. if (!process.jobOrderId) {
  256. alert(t("Invalid Job Order Id"));
  257. return;
  258. }
  259. try {
  260. setLoading(true); // 可选:已有 loading state 可复用
  261. // 1) 拉取该 JO 的所有 process,取出全部 lineId
  262. const processes = await fetchProductProcessesByJobOrderId(process.jobOrderId);
  263. const lineIds = (processes ?? [])
  264. .flatMap(p => (p as any).productProcessLines ?? [])
  265. .map(l => l.id)
  266. .filter(Boolean);
  267. // 2) 逐个调用 completeProductProcessLine
  268. for (const lineId of lineIds) {
  269. try {
  270. await completeProductProcessLine(lineId);
  271. } catch (e) {
  272. console.error("completeProductProcessLine failed for lineId:", lineId, e);
  273. }
  274. }
  275. // 3) 更新 JO 状态
  276. // await updateJo({ id: process.jobOrderId, status: "completed" });
  277. // 4) 刷新列表
  278. await fetchProcesses();
  279. } catch (e) {
  280. console.error(e);
  281. alert(t("An error has occurred. Please try again later."));
  282. } finally {
  283. setLoading(false);
  284. }
  285. }, [t, fetchProcesses, canManageUpdateJo]);
  286. const openConfirm = useCallback((message: string, action: () => Promise<void>) => {
  287. setConfirmMessage(message);
  288. setPendingConfirmAction(() => action);
  289. setConfirmOpen(true);
  290. }, []);
  291. const closeConfirm = useCallback(() => {
  292. setConfirmOpen(false);
  293. setPendingConfirmAction(null);
  294. setConfirmMessage("");
  295. setConfirmLoading(false);
  296. }, []);
  297. const onConfirm = useCallback(async () => {
  298. if (!pendingConfirmAction) return;
  299. setConfirmLoading(true);
  300. try {
  301. await pendingConfirmAction();
  302. } finally {
  303. closeConfirm();
  304. }
  305. }, [pendingConfirmAction, closeConfirm]);
  306. const closeNewModal = useCallback(() => {
  307. // const response = updateJo({ id: 1, status: "storing" });
  308. setOpenModal(false); // Close the modal first
  309. fetchProcesses();
  310. // setTimeout(() => {
  311. // }, 300); // Add a delay to avoid immediate re-trigger of useEffect
  312. }, [fetchProcesses]);
  313. const searchedItemOptions = useMemo(
  314. () =>
  315. Array.from(
  316. new Map(
  317. processes
  318. .filter((p) => !!p.itemCode)
  319. .map((p) => [p.itemCode, { itemCode: p.itemCode, itemName: p.itemName }]),
  320. ).values(),
  321. ),
  322. [processes],
  323. );
  324. const paged = useMemo(() => {
  325. if (selectedItemCodes.length === 0) return processes;
  326. return processes.filter((p) => selectedItemCodes.includes(p.itemCode));
  327. }, [processes, selectedItemCodes]);
  328. /** Reset 用 ±3 天;preFilled 用目前已套用的條件(與列表查詢一致) */
  329. const searchCriteria: Criterion<SearchParam>[] = useMemo(() => {
  330. const r = defaultPlanStartRange();
  331. return [
  332. {
  333. type: "date",
  334. label: t("Search date"),
  335. paramName: "date",
  336. defaultValue: appliedSearch.date,
  337. preFilledValue: appliedSearch.date,
  338. },
  339. {
  340. type: "text",
  341. label: "Item Code",
  342. paramName: "itemCode",
  343. preFilledValue: appliedSearch.itemCode ?? "",
  344. },
  345. {
  346. type: "text",
  347. label: "Job Order Code",
  348. paramName: "jobOrderCode",
  349. preFilledValue: appliedSearch.jobOrderCode ?? "",
  350. },
  351. {
  352. type: "select",
  353. label: "Type",
  354. paramName: "processType",
  355. options: ["all", "drink", "other"],
  356. preFilledValue: filter,
  357. },
  358. ];
  359. }, [appliedSearch, filter, t]);
  360. /** SearchBox 內部 state 只在掛載時讀 preFilled;套用搜尋後需 remount 才會與 appliedSearch 一致 */
  361. const searchBoxKey = useMemo(
  362. () =>
  363. [
  364. appliedSearch.date,
  365. appliedSearch.itemCode ?? "",
  366. appliedSearch.jobOrderCode ?? "",
  367. filter,
  368. ].join("|"),
  369. [appliedSearch, filter],
  370. );
  371. const handleSelectedItemCodesChange = useCallback(
  372. (e: SelectChangeEvent<string[]>) => {
  373. const nextValue = e.target.value;
  374. const codes = typeof nextValue === "string" ? nextValue.split(",") : nextValue;
  375. onListPersistedStateChange((prev) => ({ ...prev, selectedItemCodes: codes }));
  376. },
  377. [onListPersistedStateChange],
  378. );
  379. return (
  380. <Box>
  381. {loading ? (
  382. <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
  383. <CircularProgress />
  384. </Box>
  385. ) : (
  386. <Box>
  387. <SearchBox<SearchParam>
  388. key={searchBoxKey}
  389. criteria={searchCriteria}
  390. onSearch={handleApplySearch}
  391. onReset={handleResetSearch}
  392. extraActions={
  393. <FormControl size="small" sx={{ minWidth: 260 }}>
  394. <InputLabel>{t("Searched Item")}</InputLabel>
  395. <Select
  396. multiple
  397. value={selectedItemCodes}
  398. label={t("Item Code")}
  399. renderValue={(selected) =>
  400. (selected as string[]).length === 0 ? t("All") : (selected as string[]).join(", ")
  401. }
  402. onChange={handleSelectedItemCodesChange}
  403. >
  404. {searchedItemOptions.map((item) => (
  405. <MenuItem key={item.itemCode} value={item.itemCode}>
  406. <Checkbox checked={selectedItemCodes.includes(item.itemCode)} />
  407. <ListItemText primary={[item.itemCode, item.itemName].filter(Boolean).join(" - ")} />
  408. </MenuItem>
  409. ))}
  410. </Select>
  411. </FormControl>
  412. }
  413. />
  414. <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
  415. {t("Search date") /* 或在 zh/common.json 加鍵,例如「搜尋日期」 */}:{" "}
  416. {appliedSearch.date && dayjs(appliedSearch.date).isValid()
  417. ? dayjs(appliedSearch.date).format(OUTPUT_DATE_FORMAT)
  418. : "-"}
  419. {" | "}
  420. {t("Total job orders")}: {totalJobOrders}
  421. {selectedItemCodes.length > 0 ? ` | ${t("Filtered")}: ${paged.length}` : ""}
  422. </Typography>
  423. <Grid container spacing={2}>
  424. {paged.map((process) => {
  425. const status = String(process.status || "");
  426. const statusLower = status.toLowerCase();
  427. const displayStatus = statusLower === "in_progress" ? "processing" : status;
  428. const statusColor =
  429. statusLower === "completed"
  430. ? "success"
  431. : statusLower === "in_progress" || statusLower === "processing"
  432. ? "primary"
  433. : "default";
  434. const finishedCount =
  435. (process.lines || []).filter(
  436. (l) => String(l.status ?? "").trim().toLowerCase() === "completed"
  437. ).length;
  438. const totalCount = process.productProcessLineCount ?? process.lines?.length ?? 0;
  439. const linesWithStatus = (process.lines || []).filter(
  440. (l) => String(l.status ?? "").trim() !== ""
  441. );
  442. const dateDisplay = process.date
  443. ? dayjs(process.date as any).format(OUTPUT_DATE_FORMAT)
  444. : "-";
  445. const jobOrderCode =
  446. (process as any).jobOrderCode ??
  447. (process.jobOrderId ? `JO-${process.jobOrderId}` : "N/A");
  448. const inProgressLines = (process.lines || [])
  449. .filter(l => String(l.status ?? "").trim() !== "")
  450. .filter(l => String(l.status).toLowerCase() === "in_progress");
  451. const canQc =
  452. process.jobOrderId != null &&
  453. process.stockInLineId != null &&
  454. jobOrderQcReadyById.get(process.jobOrderId) === true;
  455. return (
  456. <Grid key={process.id} item xs={12} sm={6} md={4}>
  457. <Card
  458. sx={{
  459. minHeight: 180,
  460. maxHeight: 320,
  461. display: "flex",
  462. flexDirection: "column",
  463. border: "1px solid",
  464. borderColor: "blue",
  465. }}
  466. >
  467. <CardContent
  468. sx={{
  469. pb: 1,
  470. flexGrow: 1, // let content take remaining height
  471. overflow: "auto", // allow scroll when content exceeds
  472. }}
  473. >
  474. <Stack direction="row" justifyContent="space-between" alignItems="center">
  475. <Box sx={{ minWidth: 0 }}>
  476. <Typography variant="subtitle1">
  477. {t("Job Order")}: {jobOrderCode}
  478. </Typography>
  479. </Box>
  480. <Chip size="small" label={t(displayStatus)} color={statusColor as any} />
  481. </Stack>
  482. <Typography variant="body2" color="text.secondary">
  483. {t("Lot No")}: {process.lotNo ?? "-"}
  484. </Typography>
  485. <Typography variant="subtitle1" color="blue">
  486. {/* <strong>{t("Item Name")}:</strong> */}
  487. {process.itemCode} {process.itemName}
  488. </Typography>
  489. <Typography variant="body2" color="text.secondary">
  490. {t("Production Priority")}: {process.productionPriority}
  491. </Typography>
  492. <Typography variant="body2" color="text.secondary">
  493. {t("Required Qty")}: {process.requiredQty} ({process.uom})
  494. </Typography>
  495. <Typography variant="body2" color="text.secondary">
  496. {t("Production date")}: {process.date ? dayjs(process.date as any).format(OUTPUT_DATE_FORMAT) : "-"}
  497. </Typography>
  498. <Typography variant="body2" color="text.secondary">
  499. {t("Assume Time Need")}: {process.timeNeedToComplete} {t("minutes")}
  500. </Typography>
  501. {statusLower !== "pending" && linesWithStatus.length > 0 && (
  502. <Box sx={{ mt: 1 }}>
  503. <Typography variant="body2" fontWeight={600}>
  504. {t("Finished lines")}: {finishedCount} / {totalCount}
  505. </Typography>
  506. {inProgressLines.length > 0 && (
  507. <Box sx={{ mt: 1 }}>
  508. {inProgressLines.map(line => (
  509. <Typography key={line.id} variant="caption" color="text.secondary" display="block">
  510. {t("Operator")}: {line.operatorName || "-"} <br />
  511. {t("Equipment")}: {line.equipmentName || "-"}
  512. </Typography>
  513. ))}
  514. </Box>
  515. )}
  516. </Box>
  517. )}
  518. {statusLower == "pending" && (
  519. <Box sx={{ mt: 1 }}>
  520. <Typography variant="body2" fontWeight={600} color= "white">
  521. {t("t")}
  522. </Typography>
  523. <Box sx={{ mt: 1 }}>
  524. <Typography variant="caption" color="text.secondary" display="block">
  525. {""}
  526. </Typography>
  527. </Box>
  528. </Box>
  529. )}
  530. </CardContent>
  531. <CardActions sx={{ pt: 0.5 }}>
  532. <Button variant="contained" size="small" onClick={() => onSelectProcess(process.jobOrderId, process.id)}>
  533. {t("View Details")}
  534. </Button>
  535. <Button
  536. variant="contained"
  537. size="small"
  538. disabled={process.assignedTo != null || process.matchStatus == "completed"|| process.pickOrderStatus != "completed"}
  539. onClick={() => handleAssignPickOrder(process.pickOrderId, process.jobOrderId, process.id)}
  540. >
  541. {t("Matching Stock")}
  542. </Button>
  543. {statusLower !== "completed" && (
  544. <Button
  545. variant="contained"
  546. size="small"
  547. disabled={!canManageUpdateJo}
  548. onClick={() =>
  549. canManageUpdateJo
  550. ? openConfirm(
  551. t("Confirm to update this Job Order?"),
  552. async () => {
  553. await handleUpdateJo(process);
  554. }
  555. )
  556. : undefined
  557. }
  558. >
  559. {t("Update Job Order")}
  560. </Button>
  561. )}
  562. {canQc && (
  563. <Button variant="contained" size="small" onClick={() => handleViewStockIn(process)}>
  564. {t("view stockin")}
  565. </Button>
  566. )}
  567. <Box sx={{ flex: 1 }} />
  568. </CardActions>
  569. </Card>
  570. </Grid>
  571. );
  572. })}
  573. </Grid>
  574. <QcStockInModal
  575. session={sessionToken}
  576. open={openModal}
  577. onClose={closeNewModal}
  578. inputDetail={modalInfo}
  579. printerCombo={printerCombo}
  580. warehouse={[]}
  581. printSource="productionProcess"
  582. uiMode="default"
  583. />
  584. <Dialog open={confirmOpen} onClose={closeConfirm} maxWidth="xs" fullWidth>
  585. <DialogTitle>{t("Confirm")}</DialogTitle>
  586. <DialogContent>
  587. <Typography variant="body2">{confirmMessage}</Typography>
  588. </DialogContent>
  589. <DialogActions>
  590. <Button onClick={closeConfirm} disabled={confirmLoading}>
  591. {t("Cancel")}
  592. </Button>
  593. <Button
  594. variant="contained"
  595. onClick={onConfirm}
  596. disabled={confirmLoading || !pendingConfirmAction}
  597. >
  598. {confirmLoading ? t("Processing...") : t("Confirm")}
  599. </Button>
  600. </DialogActions>
  601. </Dialog>
  602. {totalJobOrders > 0 && (
  603. <TablePagination
  604. component="div"
  605. count={totalJobOrders}
  606. page={page}
  607. rowsPerPage={PAGE_SIZE}
  608. onPageChange={(e, p) =>
  609. onListPersistedStateChange((prev) => ({ ...prev, page: p }))
  610. }
  611. rowsPerPageOptions={[PAGE_SIZE]}
  612. />
  613. )}
  614. </Box>
  615. )}
  616. </Box>
  617. );
  618. };
  619. export default ProductProcessList;