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.
 
 

1382 lines
50 KiB

  1. "use client";
  2. import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
  3. import {
  4. Box,
  5. Button,
  6. Card,
  7. Stack,
  8. Typography,
  9. Chip,
  10. CircularProgress,
  11. Grid,
  12. FormControl,
  13. InputLabel,
  14. Select,
  15. MenuItem,
  16. Checkbox,
  17. ListItemText,
  18. SelectChangeEvent,
  19. Dialog,
  20. DialogTitle,
  21. DialogContent,
  22. DialogActions,
  23. Tabs,
  24. Tab,
  25. Tooltip,
  26. IconButton,
  27. Avatar,
  28. } from "@mui/material";
  29. import ChevronLeft from "@mui/icons-material/ChevronLeft";
  30. import ChevronRight from "@mui/icons-material/ChevronRight";
  31. import { useTranslation } from "react-i18next";
  32. import { fetchItemForPutAway } from "@/app/api/stockIn/actions";
  33. import QcStockInModal from "../Qc/QcStockInModal";
  34. import { useSession } from "next-auth/react";
  35. import { SessionWithTokens } from "@/config/authConfig";
  36. import dayjs from "dayjs";
  37. import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  38. import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox";
  39. import { AUTH, hasAbility } from "@/authorities";
  40. import {
  41. AllJoborderProductProcessInfoResponse,
  42. updateJo,
  43. fetchProductProcessesByJobOrderId,
  44. completeProductProcessLine,
  45. assignJobOrderPickOrder,
  46. fetchJoborderProductProcessesPage,
  47. JobOrderProductProcessBucketCounts,
  48. } from "@/app/api/jo/actions";
  49. import { StockInLineInput } from "@/app/api/stockIn";
  50. import { PrinterCombo } from "@/app/api/settings/printer";
  51. import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan";
  52. export type ProductionProcessListTab =
  53. | "needs_action"
  54. | "pending"
  55. | "processing"
  56. | "carried_over"
  57. | "pending_qc"
  58. | "putawayed";
  59. export type ProductionProcessListPersistedState = {
  60. date: string;
  61. itemCode: string | null;
  62. jobOrderCode: string | null;
  63. filter: "all" | "drink" | "Powder_Mixture" | "other";
  64. page: number;
  65. selectedItemCodes: string[];
  66. /**
  67. * Unified list tabs:
  68. * needs_action | pending | processing | carried_over | pending_qc | putawayed
  69. * Legacy: all → needs_action; fine pick buckets remapped to pending/processing.
  70. */
  71. pickBucket: ProductionProcessListTab | string;
  72. };
  73. interface ProductProcessListProps {
  74. onSelectProcess: (jobOrderId: number|undefined, productProcessId: number|undefined) => void;
  75. onSelectMatchingStock: (jobOrderId: number|undefined, productProcessId: number|undefined,pickOrderId: number|undefined) => void;
  76. printerCombo: PrinterCombo[];
  77. /** @deprecated Derived from pickBucket when unified tabs are used; kept for compatibility. */
  78. qcReady?: boolean;
  79. includePutaway?: boolean | null;
  80. /** all | completed | notCompleted */
  81. putawayStatus?: string | null;
  82. disableDateFilter?: boolean;
  83. listPersistedState: ProductionProcessListPersistedState;
  84. onListPersistedStateChange: React.Dispatch<
  85. React.SetStateAction<ProductionProcessListPersistedState>
  86. >;
  87. }
  88. export type SearchParam = "date" | "itemCode" | "jobOrderCode" | "processType";
  89. /** Cards per visible page: 2 rows × 3 columns. */
  90. const CARDS_PER_PAGE = 6;
  91. /** Fetch once; client slides pages of CARDS_PER_PAGE (no refetch on page change). */
  92. const FETCH_SIZE = 200;
  93. /** Include unfinished from (searchDate - LOOKBACK_DAYS) .. searchDate; picked_not_started capped at search date on backend. */
  94. const PRODUCTION_LOOKBACK_DAYS = 4;
  95. const PENDING_FINE_BUCKETS = new Set([
  96. "not_picked_not_started",
  97. "picked_not_started",
  98. ]);
  99. const PROCESSING_FINE_BUCKETS = new Set([
  100. "picked_started",
  101. "not_picked_started",
  102. ]);
  103. const EMPTY_BUCKET_COUNTS: JobOrderProductProcessBucketCounts = {
  104. notPickedNotStarted: 0,
  105. pickedNotStarted: 0,
  106. pickedStarted: 0,
  107. notPickedStarted: 0,
  108. };
  109. /** 預設依 JobOrder.planStart 搜索:今天往前 3 天~往後 3 天(含當日) */
  110. function defaultPlanStartRange() {
  111. return {
  112. from: dayjs().subtract(0, "day").format("YYYY-MM-DD"),
  113. to: dayjs().add(0, "day").format("YYYY-MM-DD"),
  114. };
  115. }
  116. export function createDefaultProductionProcessListPersistedState(): ProductionProcessListPersistedState {
  117. return {
  118. date: dayjs().format("YYYY-MM-DD"),
  119. itemCode: null,
  120. jobOrderCode: null,
  121. filter: "all",
  122. page: 0,
  123. selectedItemCodes: [],
  124. pickBucket: "needs_action",
  125. };
  126. }
  127. function normalizeListTab(raw: string | undefined | null): ProductionProcessListTab {
  128. const v = (raw || "needs_action").trim();
  129. if (
  130. v === "pending" ||
  131. v === "processing" ||
  132. v === "carried_over" ||
  133. v === "pending_qc" ||
  134. v === "putawayed" ||
  135. v === "needs_action"
  136. ) {
  137. return v;
  138. }
  139. if (v === "all") return "needs_action";
  140. if (v === "not_picked_not_started" || v === "picked_not_started") return "pending";
  141. if (v === "picked_started" || v === "not_picked_started") return "processing";
  142. return "needs_action";
  143. }
  144. function isProcessCarriedOver(
  145. p: AllJoborderProductProcessInfoResponse,
  146. searchDay: ReturnType<typeof dayjs> | null,
  147. ): boolean {
  148. if (!searchDay || !p.date || !dayjs(p.date).isValid()) return false;
  149. return dayjs(p.date).startOf("day").isBefore(searchDay);
  150. }
  151. function isPutawayCompleted(
  152. p: AllJoborderProductProcessInfoResponse,
  153. ): boolean {
  154. return String(p.stockInLineStatus ?? "").trim().toLowerCase() === "completed";
  155. }
  156. /** Waiting QC put-away (has SIL, not completed/rejected). */
  157. function isWaitingQcPutAway(
  158. p: AllJoborderProductProcessInfoResponse,
  159. ): boolean {
  160. if (p.stockInLineId == null) return false;
  161. const s = String(p.stockInLineStatus ?? "").trim().toLowerCase();
  162. return s !== "completed" && s !== "rejected";
  163. }
  164. /** FP-MTMS Version Checklist | Functions Ref. No. 40 | v1.0.3 | 2026-08-05 */
  165. const ProductProcessList: React.FC<ProductProcessListProps> = ({
  166. onSelectProcess,
  167. printerCombo,
  168. onSelectMatchingStock,
  169. disableDateFilter = false,
  170. listPersistedState,
  171. onListPersistedStateChange,
  172. }) => {
  173. const { t } = useTranslation( ["common", "productionProcess","purchaseOrder","dashboard"]);
  174. const { data: session } = useSession() as { data: SessionWithTokens | null };
  175. const sessionToken = session as SessionWithTokens | null;
  176. const [loading, setLoading] = useState(false);
  177. const [processes, setProcesses] = useState<AllJoborderProductProcessInfoResponse[]>([]);
  178. const [productionCache, setProductionCache] = useState<
  179. AllJoborderProductProcessInfoResponse[]
  180. >([]);
  181. const [pendingQcCache, setPendingQcCache] = useState<
  182. AllJoborderProductProcessInfoResponse[]
  183. >([]);
  184. const [bucketCounts, setBucketCounts] =
  185. useState<JobOrderProductProcessBucketCounts>(EMPTY_BUCKET_COUNTS);
  186. const [pendingQcCount, setPendingQcCount] = useState(0);
  187. const [putawayedCount, setPutawayedCount] = useState(0);
  188. const [carriedOverCount, setCarriedOverCount] = useState(0);
  189. const [openModal, setOpenModal] = useState<boolean>(false);
  190. const [modalInfo, setModalInfo] = useState<StockInLineInput>();
  191. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  192. const abilities = session?.abilities ?? session?.user?.abilities ?? [];
  193. /** 完成工單:僅 PRODUCT_PROCESS(工單 生產流程 完成工單);不含 ADMIN 以免群組 ADMIN 繞過勾選 */
  194. const canManageUpdateJo = hasAbility(abilities, AUTH.PRODUCT_PROCESS);
  195. type ProcessFilter = "all" | "drink" | "Powder_Mixture" | "other";
  196. const listTab = normalizeListTab(listPersistedState.pickBucket);
  197. const isProductionTab =
  198. listTab === "needs_action" || listTab === "pending" || listTab === "processing";
  199. const isCarriedOverTab = listTab === "carried_over";
  200. const qcReady = listTab === "pending_qc" || listTab === "putawayed";
  201. const putawayStatus =
  202. listTab === "putawayed"
  203. ? "completed"
  204. : listTab === "pending_qc"
  205. ? "notCompleted"
  206. : null;
  207. const includePutaway = qcReady ? true : null;
  208. /** Production unfinished tabs + pending QC + off-plan: carry-over window. */
  209. const enableCarryOver =
  210. !disableDateFilter &&
  211. (isProductionTab || listTab === "pending_qc" || isCarriedOverTab);
  212. const appliedSearch = useMemo(
  213. () => ({
  214. date: listPersistedState.date,
  215. itemCode: listPersistedState.itemCode,
  216. jobOrderCode: listPersistedState.jobOrderCode,
  217. }),
  218. [
  219. listPersistedState.date,
  220. listPersistedState.itemCode,
  221. listPersistedState.jobOrderCode,
  222. ],
  223. );
  224. const filter = listPersistedState.filter;
  225. const page = listPersistedState.page;
  226. const selectedItemCodes = listPersistedState.selectedItemCodes;
  227. const searchDay = useMemo(
  228. () => (appliedSearch.date ? dayjs(appliedSearch.date).startOf("day") : null),
  229. [appliedSearch.date],
  230. );
  231. const [totalJobOrders, setTotalJobOrders] = useState(0);
  232. // Generic confirm dialog for actions (update job order / etc.)
  233. const [confirmOpen, setConfirmOpen] = useState(false);
  234. const [confirmMessage, setConfirmMessage] = useState("");
  235. const [confirmLoading, setConfirmLoading] = useState(false);
  236. const [pendingConfirmAction, setPendingConfirmAction] = useState<null | (() => Promise<void>)>(null);
  237. // QC 的业务判定:同一个 jobOrder 下,所有 productProcess 的所有 lines 都必须是 Completed/Pass
  238. // 才允许打开 QcStockInModal(避免仅某个 productProcess 完成就提前出现 view stockin)。
  239. const jobOrderQcReadyById = useMemo(() => {
  240. const lineDone = (status: unknown) => {
  241. const s = String(status ?? "").trim().toLowerCase();
  242. return s === "completed" || s === "pass";
  243. };
  244. const byJobOrder = new Map<number, AllJoborderProductProcessInfoResponse[]>();
  245. for (const p of processes) {
  246. if (p.jobOrderId == null) continue;
  247. const arr = byJobOrder.get(p.jobOrderId) ?? [];
  248. arr.push(p);
  249. byJobOrder.set(p.jobOrderId, arr);
  250. }
  251. const result = new Map<number, boolean>();
  252. const isDone = (status: unknown) => {
  253. const s = String(status ?? "").trim().toLowerCase();
  254. return s === "completed" || s === "pass";
  255. };
  256. byJobOrder.forEach((jobOrderProcesses, jobOrderId) => {
  257. const hasStockInLine = jobOrderProcesses.some((p) => p.stockInLineId != null);
  258. const packingProcesses = jobOrderProcesses.filter(
  259. (p) => String((p as any).code ?? "").trim() === "包裝"
  260. );
  261. const nonPackingProcesses = jobOrderProcesses.filter(
  262. (p) => String((p as any).code ?? "").trim() !== "包裝"
  263. );
  264. const allNonPackingDone =
  265. nonPackingProcesses.length === 0 ||
  266. nonPackingProcesses.every((p) => {
  267. const lines = p.lines ?? [];
  268. return lines.length > 0 && lines.every((l) => isDone(l.status));
  269. });
  270. const hasOnePackingDone =
  271. packingProcesses.length > 0 &&
  272. packingProcesses.some((p) => {
  273. const lines = p.lines ?? [];
  274. return lines.some((l) => isDone(l.status));
  275. });
  276. const packingOk = packingProcesses.length === 0 ? true : hasOnePackingDone;
  277. result.set(jobOrderId, hasStockInLine && allNonPackingDone && packingOk);
  278. });
  279. return result;
  280. }, [processes]);
  281. const handleAssignPickOrder = useCallback(async (pickOrderId: number, jobOrderId?: number, productProcessId?: number) => {
  282. if (!currentUserId) {
  283. alert(t("Unable to get user ID"));
  284. return;
  285. }
  286. try {
  287. console.log("🔄 Assigning pick order:", pickOrderId, "to user:", currentUserId);
  288. // 调用分配 API 并读取响应
  289. const assignResult = await assignJobOrderPickOrder(pickOrderId, currentUserId);
  290. console.log("📦 Assign result:", assignResult);
  291. // 检查分配是否成功
  292. if (assignResult.message === "Successfully assigned") {
  293. console.log("✅ Successfully assigned pick order");
  294. console.log("✅ Pick order ID:", assignResult.id);
  295. console.log("✅ Pick order code:", assignResult.code);
  296. // 分配成功后,导航到 second scan 页面
  297. if (onSelectMatchingStock && jobOrderId) {
  298. onSelectMatchingStock(jobOrderId, productProcessId,pickOrderId);
  299. } else {
  300. alert(t("Assignment successful"));
  301. }
  302. } else {
  303. // 分配失败
  304. console.error("Assignment failed:", assignResult.message);
  305. alert(t(`Assignment failed: ${assignResult.message || "Unknown error"}`));
  306. }
  307. } catch (error: any) {
  308. console.error(" Error assigning pick order:", error);
  309. alert(t(`Unknown error: ${error?.message || "Unknown error"}。Please try again later.`));
  310. }
  311. }, [currentUserId, t, onSelectMatchingStock]);
  312. const handleViewStockIn = useCallback((process: AllJoborderProductProcessInfoResponse) => {
  313. if (!process.stockInLineId) {
  314. alert(t("Invalid Stock In Line Id"));
  315. return;
  316. }
  317. setModalInfo({
  318. id: process.stockInLineId,
  319. //itemId: process.itemId, // 如果 process 中有 itemId,添加这一行
  320. //expiryDate: dayjs().add(1, "month").format(OUTPUT_DATE_FORMAT),
  321. });
  322. setOpenModal(true);
  323. }, [t]);
  324. const handleApplySearch = useCallback(
  325. (inputs: Record<SearchParam | `${SearchParam}To`, string>) => {
  326. const selectedProcessType = (inputs.processType || "all") as ProcessFilter;
  327. onListPersistedStateChange((prev) => ({
  328. ...prev,
  329. filter: selectedProcessType,
  330. date: disableDateFilter ? "" : (inputs.date || "").trim(),
  331. itemCode: inputs.itemCode?.trim() ? inputs.itemCode.trim() : null,
  332. jobOrderCode: inputs.jobOrderCode?.trim() ? inputs.jobOrderCode.trim() : null,
  333. selectedItemCodes: [],
  334. page: 0,
  335. }));
  336. },
  337. [disableDateFilter, onListPersistedStateChange],
  338. );
  339. const handleResetSearch = useCallback(() => {
  340. onListPersistedStateChange((prev) => ({
  341. ...prev,
  342. filter: "all",
  343. date: disableDateFilter ? "" : defaultPlanStartRange().from,
  344. itemCode: null,
  345. jobOrderCode: null,
  346. selectedItemCodes: [],
  347. page: 0,
  348. pickBucket: "needs_action",
  349. }));
  350. }, [disableDateFilter, onListPersistedStateChange]);
  351. const fetchProcesses = useCallback(async () => {
  352. setLoading(true);
  353. try {
  354. const typeParam = filter === "all" ? undefined : filter;
  355. const base = {
  356. date: disableDateFilter ? undefined : appliedSearch.date,
  357. itemCode: appliedSearch.itemCode,
  358. jobOrderCode: appliedSearch.jobOrderCode,
  359. type: typeParam,
  360. page: 0,
  361. size: FETCH_SIZE,
  362. };
  363. if (isCarriedOverTab) {
  364. const [prod, pendingQc] = await Promise.all([
  365. fetchJoborderProductProcessesPage({
  366. ...base,
  367. qcReady: false,
  368. lookbackDays: enableCarryOver ? PRODUCTION_LOOKBACK_DAYS : undefined,
  369. bucket: "all",
  370. }),
  371. fetchJoborderProductProcessesPage({
  372. ...base,
  373. qcReady: true,
  374. includePutaway: true,
  375. putawayStatus: "notCompleted",
  376. lookbackDays: enableCarryOver ? PRODUCTION_LOOKBACK_DAYS : undefined,
  377. }),
  378. ]);
  379. const prodContent = (prod?.content || []).filter((p) => !isPutawayCompleted(p));
  380. const qcContent = (pendingQc?.content || []).filter((p) => !isPutawayCompleted(p));
  381. setProductionCache(prodContent);
  382. setPendingQcCache(qcContent);
  383. if (prod?.bucketCounts) setBucketCounts(prod.bucketCounts);
  384. setPendingQcCount(pendingQc?.totalJobOrders || 0);
  385. setCarriedOverCount(prod?.carriedOverCount ?? 0);
  386. const search = appliedSearch.date
  387. ? dayjs(appliedSearch.date).startOf("day")
  388. : null;
  389. const merged = [...qcContent, ...prodContent].filter((p) =>
  390. isProcessCarriedOver(p, search),
  391. );
  392. // Dedupe by jobOrderId (prefer waiting-QC row when it has stockInLineId)
  393. const byJo = new Map<number, AllJoborderProductProcessInfoResponse>();
  394. for (const p of merged) {
  395. if (p.jobOrderId == null) continue;
  396. const existing = byJo.get(p.jobOrderId);
  397. if (!existing) {
  398. byJo.set(p.jobOrderId, p);
  399. } else if (existing.stockInLineId == null && p.stockInLineId != null) {
  400. byJo.set(p.jobOrderId, p);
  401. }
  402. }
  403. const content = Array.from(byJo.values());
  404. setProcesses(content);
  405. setTotalJobOrders(content.length);
  406. return;
  407. }
  408. // Production tabs share one fetch (bucket=all); pending/processing filter client-side.
  409. const data = await fetchJoborderProductProcessesPage({
  410. ...base,
  411. qcReady,
  412. includePutaway,
  413. putawayStatus,
  414. lookbackDays: enableCarryOver ? PRODUCTION_LOOKBACK_DAYS : undefined,
  415. bucket: isProductionTab ? "all" : undefined,
  416. });
  417. const content = data?.content || [];
  418. setProcesses(content);
  419. setTotalJobOrders(data?.totalJobOrders || 0);
  420. if (isProductionTab) {
  421. setProductionCache(content);
  422. if (data?.bucketCounts) setBucketCounts(data.bucketCounts);
  423. }
  424. if (qcReady && putawayStatus === "notCompleted") {
  425. setPendingQcCache(content);
  426. setPendingQcCount(data?.totalJobOrders || 0);
  427. }
  428. if (qcReady && putawayStatus === "completed") {
  429. setPutawayedCount(data?.totalJobOrders || 0);
  430. }
  431. setCarriedOverCount(data?.carriedOverCount ?? 0);
  432. } catch (e) {
  433. console.error(e);
  434. setProcesses([]);
  435. setTotalJobOrders(0);
  436. if (isProductionTab) setBucketCounts(EMPTY_BUCKET_COUNTS);
  437. setCarriedOverCount(0);
  438. } finally {
  439. setLoading(false);
  440. }
  441. }, [
  442. appliedSearch,
  443. disableDateFilter,
  444. filter,
  445. qcReady,
  446. includePutaway,
  447. putawayStatus,
  448. enableCarryOver,
  449. isProductionTab,
  450. isCarriedOverTab,
  451. ]);
  452. useEffect(() => {
  453. fetchProcesses();
  454. }, [fetchProcesses]);
  455. /** Keep production + QC tab badges fresh even when not on that tab. */
  456. useEffect(() => {
  457. let cancelled = false;
  458. const typeParam = filter === "all" ? undefined : filter;
  459. const base = {
  460. date: disableDateFilter ? undefined : appliedSearch.date,
  461. itemCode: appliedSearch.itemCode,
  462. jobOrderCode: appliedSearch.jobOrderCode,
  463. type: typeParam,
  464. page: 0,
  465. size: FETCH_SIZE,
  466. };
  467. (async () => {
  468. try {
  469. const [prod, pendingQc, putawayed] = await Promise.all([
  470. fetchJoborderProductProcessesPage({
  471. ...base,
  472. qcReady: false,
  473. lookbackDays: disableDateFilter ? undefined : PRODUCTION_LOOKBACK_DAYS,
  474. bucket: "all",
  475. }),
  476. fetchJoborderProductProcessesPage({
  477. ...base,
  478. qcReady: true,
  479. includePutaway: true,
  480. putawayStatus: "notCompleted",
  481. lookbackDays: disableDateFilter ? undefined : PRODUCTION_LOOKBACK_DAYS,
  482. }),
  483. fetchJoborderProductProcessesPage({
  484. ...base,
  485. size: 1,
  486. qcReady: true,
  487. includePutaway: true,
  488. putawayStatus: "completed",
  489. }),
  490. ]);
  491. if (cancelled) return;
  492. if (prod?.bucketCounts) setBucketCounts(prod.bucketCounts);
  493. if (prod?.content) {
  494. setProductionCache(prod.content.filter((p) => !isPutawayCompleted(p)));
  495. }
  496. if (pendingQc?.content) {
  497. setPendingQcCache(
  498. pendingQc.content.filter((p) => !isPutawayCompleted(p)),
  499. );
  500. }
  501. setPendingQcCount(pendingQc?.totalJobOrders || 0);
  502. setPutawayedCount(putawayed?.totalJobOrders || 0);
  503. if (prod?.carriedOverCount != null) setCarriedOverCount(prod.carriedOverCount);
  504. } catch (e) {
  505. console.error(e);
  506. }
  507. })();
  508. return () => {
  509. cancelled = true;
  510. };
  511. }, [appliedSearch, disableDateFilter, filter]);
  512. const handleListTabChange = useCallback(
  513. (_: React.SyntheticEvent, value: string) => {
  514. const next = normalizeListTab(value);
  515. onListPersistedStateChange((prev) => ({
  516. ...prev,
  517. pickBucket: next,
  518. page: 0,
  519. }));
  520. },
  521. [onListPersistedStateChange],
  522. );
  523. const pendingCount =
  524. bucketCounts.notPickedNotStarted + bucketCounts.pickedNotStarted;
  525. const processingCount =
  526. bucketCounts.pickedStarted + bucketCounts.notPickedStarted;
  527. const needsActionCount = pendingCount + processingCount;
  528. const offPlanTabCount = useMemo(() => {
  529. if (!searchDay) return 0;
  530. const ids = new Set<number>();
  531. for (const p of productionCache) {
  532. if (
  533. !isPutawayCompleted(p) &&
  534. isProcessCarriedOver(p, searchDay) &&
  535. p.jobOrderId != null
  536. ) {
  537. ids.add(p.jobOrderId);
  538. }
  539. }
  540. for (const p of pendingQcCache) {
  541. if (
  542. !isPutawayCompleted(p) &&
  543. isProcessCarriedOver(p, searchDay) &&
  544. p.jobOrderId != null
  545. ) {
  546. ids.add(p.jobOrderId);
  547. }
  548. }
  549. return ids.size;
  550. }, [productionCache, pendingQcCache, searchDay]);
  551. const filteredProcesses = useMemo(() => {
  552. let list = processes;
  553. if (listTab === "pending") {
  554. list = list.filter((p) =>
  555. PENDING_FINE_BUCKETS.has(String(p.pickProcessBucket ?? "")),
  556. );
  557. } else if (listTab === "processing") {
  558. list = list.filter((p) =>
  559. PROCESSING_FINE_BUCKETS.has(String(p.pickProcessBucket ?? "")),
  560. );
  561. }
  562. // carried_over: fetch already filtered to off-plan only
  563. if (selectedItemCodes.length === 0) return list;
  564. return list.filter((p) => selectedItemCodes.includes(p.itemCode));
  565. }, [processes, selectedItemCodes, listTab]);
  566. const displayTotalJobOrders = isCarriedOverTab
  567. ? filteredProcesses.length
  568. : isProductionTab
  569. ? listTab === "pending"
  570. ? pendingCount
  571. : listTab === "processing"
  572. ? processingCount
  573. : totalJobOrders || needsActionCount
  574. : totalJobOrders;
  575. const displayCarriedOverCount = useMemo(() => {
  576. if (!enableCarryOver || !searchDay) return 0;
  577. if (!isProductionTab || listTab === "needs_action") return carriedOverCount;
  578. return filteredProcesses.filter((p) => {
  579. if (!p.date || !dayjs(p.date).isValid()) return false;
  580. return dayjs(p.date).startOf("day").isBefore(searchDay);
  581. }).length;
  582. }, [
  583. enableCarryOver,
  584. searchDay,
  585. isProductionTab,
  586. listTab,
  587. carriedOverCount,
  588. filteredProcesses,
  589. ]);
  590. const pageChunks = useMemo(() => {
  591. const chunks: AllJoborderProductProcessInfoResponse[][] = [];
  592. for (let i = 0; i < filteredProcesses.length; i += CARDS_PER_PAGE) {
  593. chunks.push(filteredProcesses.slice(i, i + CARDS_PER_PAGE));
  594. }
  595. return chunks.length > 0 ? chunks : [[]];
  596. }, [filteredProcesses]);
  597. const totalPages = pageChunks.length;
  598. const safePage = Math.min(page, Math.max(0, totalPages - 1));
  599. const scrollerRef = useRef<HTMLDivElement | null>(null);
  600. const scrollSyncLockRef = useRef(false);
  601. const scrollToPage = useCallback(
  602. (targetPage: number, behavior: ScrollBehavior = "smooth") => {
  603. const el = scrollerRef.current;
  604. if (!el) return;
  605. const clamped = Math.max(0, Math.min(targetPage, totalPages - 1));
  606. scrollSyncLockRef.current = true;
  607. el.scrollTo({ left: clamped * el.clientWidth, behavior });
  608. onListPersistedStateChange((prev) =>
  609. prev.page === clamped ? prev : { ...prev, page: clamped },
  610. );
  611. window.setTimeout(() => {
  612. scrollSyncLockRef.current = false;
  613. }, behavior === "smooth" ? 450 : 50);
  614. },
  615. [totalPages, onListPersistedStateChange],
  616. );
  617. const goPrevPage = useCallback(() => {
  618. if (safePage <= 0) return;
  619. scrollToPage(safePage - 1);
  620. }, [safePage, scrollToPage]);
  621. const goNextPage = useCallback(() => {
  622. if (safePage + 1 >= totalPages) return;
  623. scrollToPage(safePage + 1);
  624. }, [safePage, totalPages, scrollToPage]);
  625. const handleScrollerScroll = useCallback(() => {
  626. if (scrollSyncLockRef.current) return;
  627. const el = scrollerRef.current;
  628. if (!el || el.clientWidth <= 0) return;
  629. const nextPage = Math.round(el.scrollLeft / el.clientWidth);
  630. const clamped = Math.max(0, Math.min(nextPage, totalPages - 1));
  631. if (clamped !== page) {
  632. onListPersistedStateChange((prev) =>
  633. prev.page === clamped ? prev : { ...prev, page: clamped },
  634. );
  635. }
  636. }, [page, totalPages, onListPersistedStateChange]);
  637. // After data reload, jump to persisted page without animation.
  638. useEffect(() => {
  639. const el = scrollerRef.current;
  640. if (!el || loading) return;
  641. const clamped = Math.min(page, Math.max(0, totalPages - 1));
  642. scrollSyncLockRef.current = true;
  643. el.scrollTo({ left: clamped * el.clientWidth, behavior: "auto" });
  644. window.setTimeout(() => {
  645. scrollSyncLockRef.current = false;
  646. }, 50);
  647. }, [loading, filteredProcesses, totalPages]); // eslint-disable-line react-hooks/exhaustive-deps
  648. const renderBucketTabLabel = useCallback(
  649. (labelKey: string, count: number) =>
  650. count > 0 ? `${t(labelKey)} (${count})` : t(labelKey),
  651. [t],
  652. );
  653. const handleUpdateJo = useCallback(async (process: AllJoborderProductProcessInfoResponse) => {
  654. if (!canManageUpdateJo) return;
  655. if (!process.jobOrderId) {
  656. alert(t("Invalid Job Order Id"));
  657. return;
  658. }
  659. try {
  660. setLoading(true); // 可选:已有 loading state 可复用
  661. // 1) 拉取该 JO 的所有 process,取出全部 lineId
  662. const processes = await fetchProductProcessesByJobOrderId(process.jobOrderId);
  663. const lineIds = (processes ?? [])
  664. .flatMap(p => (p as any).productProcessLines ?? [])
  665. .map(l => l.id)
  666. .filter(Boolean);
  667. // 2) 逐个调用 completeProductProcessLine
  668. for (const lineId of lineIds) {
  669. try {
  670. await completeProductProcessLine(lineId);
  671. } catch (e) {
  672. console.error("completeProductProcessLine failed for lineId:", lineId, e);
  673. }
  674. }
  675. // 3) 更新 JO 状态
  676. // await updateJo({ id: process.jobOrderId, status: "completed" });
  677. // 4) 刷新列表
  678. await fetchProcesses();
  679. } catch (e) {
  680. console.error(e);
  681. alert(t("An error has occurred. Please try again later."));
  682. } finally {
  683. setLoading(false);
  684. }
  685. }, [t, fetchProcesses, canManageUpdateJo]);
  686. const openConfirm = useCallback((message: string, action: () => Promise<void>) => {
  687. setConfirmMessage(message);
  688. setPendingConfirmAction(() => action);
  689. setConfirmOpen(true);
  690. }, []);
  691. const closeConfirm = useCallback(() => {
  692. setConfirmOpen(false);
  693. setPendingConfirmAction(null);
  694. setConfirmMessage("");
  695. setConfirmLoading(false);
  696. }, []);
  697. const onConfirm = useCallback(async () => {
  698. if (!pendingConfirmAction) return;
  699. setConfirmLoading(true);
  700. try {
  701. await pendingConfirmAction();
  702. } finally {
  703. closeConfirm();
  704. }
  705. }, [pendingConfirmAction, closeConfirm]);
  706. const closeNewModal = useCallback(() => {
  707. // const response = updateJo({ id: 1, status: "storing" });
  708. setOpenModal(false); // Close the modal first
  709. // fetchProcesses();
  710. // setTimeout(() => {
  711. // }, 300); // Add a delay to avoid immediate re-trigger of useEffect
  712. }, [fetchProcesses]);
  713. const searchedItemOptions = useMemo(
  714. () =>
  715. Array.from(
  716. new Map(
  717. processes
  718. .filter((p) => !!p.itemCode)
  719. .map((p) => [p.itemCode, { itemCode: p.itemCode, itemName: p.itemName }]),
  720. ).values(),
  721. ),
  722. [processes],
  723. );
  724. /** Reset 用 ±3 天;preFilled 用目前已套用的條件(與列表查詢一致) */
  725. const searchCriteria: Criterion<SearchParam>[] = useMemo(() => {
  726. const base: Criterion<SearchParam>[] = [
  727. ...(disableDateFilter
  728. ? []
  729. : [{
  730. type: "date",
  731. label: t("Search date"),
  732. paramName: "date",
  733. defaultValue: appliedSearch.date,
  734. preFilledValue: appliedSearch.date,
  735. } as Criterion<SearchParam>]),
  736. {
  737. type: "text",
  738. label: t("Item Code"),
  739. paramName: "itemCode",
  740. preFilledValue: appliedSearch.itemCode ?? "",
  741. },
  742. {
  743. type: "text",
  744. label: t("Job Order Code"),
  745. paramName: "jobOrderCode",
  746. preFilledValue: appliedSearch.jobOrderCode ?? "",
  747. },
  748. {
  749. type: "select",
  750. label: t("Process Type"),
  751. paramName: "processType",
  752. options: ["all", "drink", "Powder_Mixture", "other"],
  753. preFilledValue: filter,
  754. },
  755. ];
  756. return base;
  757. }, [appliedSearch, disableDateFilter, filter, t]);
  758. /** SearchBox 內部 state 只在掛載時讀 preFilled;套用搜索後需 remount 才會與 appliedSearch 一致 */
  759. const searchBoxKey = useMemo(
  760. () =>
  761. [
  762. disableDateFilter ? "" : appliedSearch.date,
  763. appliedSearch.itemCode ?? "",
  764. appliedSearch.jobOrderCode ?? "",
  765. filter,
  766. ].join("|"),
  767. [appliedSearch, disableDateFilter, filter],
  768. );
  769. const handleSelectedItemCodesChange = useCallback(
  770. (e: SelectChangeEvent<string[]>) => {
  771. const nextValue = e.target.value;
  772. const codes = typeof nextValue === "string" ? nextValue.split(",") : nextValue;
  773. onListPersistedStateChange((prev) => ({ ...prev, selectedItemCodes: codes }));
  774. },
  775. [onListPersistedStateChange],
  776. );
  777. return (
  778. <Box>
  779. {loading ? (
  780. <Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
  781. <CircularProgress />
  782. </Box>
  783. ) : (
  784. <Box>
  785. <SearchBox<SearchParam>
  786. key={searchBoxKey}
  787. criteria={searchCriteria}
  788. onSearch={handleApplySearch}
  789. onReset={handleResetSearch}
  790. extraActions={
  791. <FormControl size="small" sx={{ minWidth: 260 }}>
  792. <InputLabel>{t("Searched Item")}</InputLabel>
  793. <Select
  794. multiple
  795. value={selectedItemCodes}
  796. label={t("Item Code")}
  797. renderValue={(selected) =>
  798. (selected as string[]).length === 0 ? t("All") : (selected as string[]).join(", ")
  799. }
  800. onChange={handleSelectedItemCodesChange}
  801. >
  802. {searchedItemOptions.map((item) => (
  803. <MenuItem key={item.itemCode} value={item.itemCode}>
  804. <Checkbox checked={selectedItemCodes.includes(item.itemCode)} />
  805. <ListItemText primary={[item.itemCode, item.itemName].filter(Boolean).join(" - ")} />
  806. </MenuItem>
  807. ))}
  808. </Select>
  809. </FormControl>
  810. }
  811. />
  812. <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
  813. {!disableDateFilter && (
  814. <>
  815. {t("Search date")}:{" "}
  816. {appliedSearch.date && dayjs(appliedSearch.date).isValid()
  817. ? dayjs(appliedSearch.date).format(OUTPUT_DATE_FORMAT)
  818. : "-"}
  819. {" | "}
  820. </>
  821. )}
  822. {t("Total job orders")}: {displayTotalJobOrders}
  823. {enableCarryOver && displayCarriedOverCount > 0
  824. ? ` | ${t("Including carried over")}: ${displayCarriedOverCount}`
  825. : ""}
  826. {selectedItemCodes.length > 0 ? ` | ${t("Filtered")}: ${filteredProcesses.length}` : ""}
  827. </Typography>
  828. <Tabs
  829. value={listTab}
  830. onChange={handleListTabChange}
  831. variant="scrollable"
  832. scrollButtons="auto"
  833. sx={{
  834. mb: 2,
  835. borderBottom: 1,
  836. borderColor: "divider",
  837. "& .MuiTabs-flexContainer": {
  838. columnGap: 2,
  839. rowGap: 1,
  840. },
  841. "& .MuiTab-root": {
  842. overflow: "visible",
  843. minWidth: "auto",
  844. px: 2,
  845. },
  846. }}
  847. >
  848. <Tab
  849. value="needs_action"
  850. label={renderBucketTabLabel("Needs action", needsActionCount)}
  851. />
  852. <Tab
  853. value="pending"
  854. label={renderBucketTabLabel("pending", pendingCount)}
  855. />
  856. <Tab
  857. value="processing"
  858. label={renderBucketTabLabel("Processing", processingCount)}
  859. />
  860. <Tab
  861. value="carried_over"
  862. label={renderBucketTabLabel(
  863. "Off-plan unfinished",
  864. offPlanTabCount,
  865. )}
  866. />
  867. <Tab
  868. value="pending_qc"
  869. label={renderBucketTabLabel(
  870. "Waiting QC Put Away",
  871. pendingQcCount,
  872. )}
  873. />
  874. <Tab
  875. value="putawayed"
  876. label={renderBucketTabLabel("Put Awayed", putawayedCount)}
  877. />
  878. </Tabs>
  879. <Box
  880. sx={{
  881. display: "flex",
  882. alignItems: "stretch",
  883. gap: 1,
  884. }}
  885. onKeyDown={(e) => {
  886. if (e.key === "ArrowLeft") goPrevPage();
  887. if (e.key === "ArrowRight") goNextPage();
  888. }}
  889. >
  890. <IconButton
  891. aria-label={t("Previous page")}
  892. onClick={goPrevPage}
  893. disabled={safePage <= 0 || filteredProcesses.length === 0}
  894. sx={{ alignSelf: "center" }}
  895. >
  896. <ChevronLeft />
  897. </IconButton>
  898. <Box
  899. ref={scrollerRef}
  900. onScroll={handleScrollerScroll}
  901. sx={{
  902. flex: 1,
  903. minWidth: 0,
  904. display: "flex",
  905. overflowX: "auto",
  906. scrollSnapType: "x mandatory",
  907. scrollBehavior: "smooth",
  908. WebkitOverflowScrolling: "touch",
  909. scrollbarWidth: "none",
  910. msOverflowStyle: "none",
  911. "&::-webkit-scrollbar": { display: "none" },
  912. }}
  913. >
  914. {pageChunks.map((chunk, pageIndex) => (
  915. <Box
  916. key={`page-${pageIndex}`}
  917. sx={{
  918. minWidth: "100%",
  919. width: "100%",
  920. flexShrink: 0,
  921. scrollSnapAlign: "start",
  922. scrollSnapStop: "always",
  923. px: 0.5,
  924. boxSizing: "border-box",
  925. }}
  926. >
  927. <Grid container spacing={2} alignItems="stretch">
  928. {chunk.map((process) => {
  929. const status = String(process.status || "");
  930. const statusLower = status.toLowerCase();
  931. const displayStatus =
  932. statusLower === "in_progress" ? "processing" : status;
  933. const bucket = String(process.pickProcessBucket ?? "");
  934. const waitingQc = isWaitingQcPutAway(process);
  935. // Avoid t("completed") → purchaseOrder「已上架」; use explicit keys.
  936. let chipLabel: string;
  937. let statusColor: "success" | "warning" | "primary" | "default";
  938. if (listTab === "putawayed") {
  939. chipLabel = t("Put Awayed");
  940. statusColor = "success";
  941. } else if (listTab === "pending_qc" || (isCarriedOverTab && waitingQc)) {
  942. chipLabel = t("Waiting QC Put Away");
  943. statusColor = "warning";
  944. } else if (isCarriedOverTab && PENDING_FINE_BUCKETS.has(bucket)) {
  945. chipLabel = t("pending");
  946. statusColor = "default";
  947. } else if (isCarriedOverTab && PROCESSING_FINE_BUCKETS.has(bucket)) {
  948. chipLabel = t("processing");
  949. statusColor = "primary";
  950. } else if (qcReady) {
  951. chipLabel =
  952. putawayStatus === "completed"
  953. ? t("Put Awayed")
  954. : t("Waiting QC Put Away");
  955. statusColor =
  956. putawayStatus === "completed" ? "success" : "warning";
  957. } else if (statusLower === "completed") {
  958. chipLabel = t("Completed");
  959. statusColor = "success";
  960. } else {
  961. chipLabel = t(displayStatus);
  962. statusColor =
  963. statusLower === "in_progress" ||
  964. statusLower === "processing"
  965. ? "primary"
  966. : "default";
  967. }
  968. const jobOrderCode =
  969. (process as any).jobOrderCode ??
  970. (process.jobOrderId ? `JO-${process.jobOrderId}` : "N/A");
  971. const canQc =
  972. process.jobOrderId != null &&
  973. process.stockInLineId != null &&
  974. jobOrderQcReadyById.get(process.jobOrderId) === true;
  975. const joDay = process.date
  976. ? dayjs(process.date).startOf("day")
  977. : null;
  978. const isCarriedOver =
  979. isCarriedOverTab ||
  980. Boolean(
  981. enableCarryOver &&
  982. searchDay?.isValid() &&
  983. joDay?.isValid() &&
  984. joDay.isBefore(searchDay),
  985. );
  986. const bomDescription = process.bomDescription
  987. ? String(process.bomDescription).trim()
  988. : "";
  989. const bomType = process.bomType
  990. ? String(process.bomType).trim()
  991. : "";
  992. const chipSx = {
  993. flexShrink: 0,
  994. height: 28,
  995. borderRadius: "14px",
  996. "& .MuiChip-label": {
  997. typography: "body2",
  998. px: 1.25,
  999. lineHeight: 1.2,
  1000. },
  1001. } as const;
  1002. return (
  1003. <Grid
  1004. key={process.id}
  1005. item
  1006. xs={12}
  1007. sm={6}
  1008. md={4}
  1009. sx={{ display: "flex" }}
  1010. >
  1011. <Card
  1012. sx={{
  1013. width: "100%",
  1014. height: "100%",
  1015. display: "flex",
  1016. flexDirection: "column",
  1017. border: "1px solid",
  1018. borderColor: isCarriedOver ? "warning.main" : "divider",
  1019. borderRadius: 2,
  1020. boxShadow: "none",
  1021. bgcolor: isCarriedOver
  1022. ? "warning.light"
  1023. : "background.paper",
  1024. }}
  1025. >
  1026. <Box
  1027. sx={{
  1028. p: 2,
  1029. flexGrow: 1,
  1030. display: "flex",
  1031. flexDirection: "column",
  1032. minHeight: 0,
  1033. }}
  1034. >
  1035. <Stack
  1036. direction="row"
  1037. alignItems="flex-start"
  1038. spacing={0.75}
  1039. >
  1040. <Typography
  1041. variant="body1"
  1042. color="text.primary"
  1043. fontWeight={600}
  1044. title={
  1045. [process.itemCode, process.itemName]
  1046. .filter(Boolean)
  1047. .join(" ") || undefined
  1048. }
  1049. sx={{
  1050. flex: 1,
  1051. minWidth: 0,
  1052. display: "-webkit-box",
  1053. WebkitLineClamp: 2,
  1054. WebkitBoxOrient: "vertical",
  1055. overflow: "hidden",
  1056. lineHeight: 1.35,
  1057. }}
  1058. >
  1059. {[process.itemCode, process.itemName].filter(Boolean).join(" ") || "-"}
  1060. </Typography>
  1061. {isCarriedOver ? (
  1062. <Tooltip title={t("Carried over from past day")}>
  1063. <Avatar
  1064. aria-label={t("Carried over from past day")}
  1065. sx={{
  1066. width: 28,
  1067. height: 28,
  1068. flexShrink: 0,
  1069. bgcolor: "error.main",
  1070. color: "error.contrastText",
  1071. fontSize: "1rem",
  1072. fontWeight: 800,
  1073. }}
  1074. >
  1075. !
  1076. </Avatar>
  1077. </Tooltip>
  1078. ) : null}
  1079. </Stack>
  1080. <Stack
  1081. direction="row"
  1082. alignItems="center"
  1083. spacing={0.75}
  1084. flexWrap="nowrap"
  1085. useFlexGap
  1086. sx={{
  1087. mt: 0.75,
  1088. minWidth: 0,
  1089. overflowX: "auto",
  1090. scrollbarWidth: "none",
  1091. "&::-webkit-scrollbar": { display: "none" },
  1092. }}
  1093. >
  1094. <Tooltip title={t("Production Priority")}>
  1095. <Avatar
  1096. aria-label={`${t("Production Priority")}: ${process.productionPriority ?? "-"}`}
  1097. sx={{
  1098. width: 28,
  1099. height: 28,
  1100. flexShrink: 0,
  1101. bgcolor: "primary.main",
  1102. color: "primary.contrastText",
  1103. fontSize: "0.8rem",
  1104. fontWeight: 700,
  1105. }}
  1106. >
  1107. {process.productionPriority ?? "-"}
  1108. </Avatar>
  1109. </Tooltip>
  1110. {bomDescription ? (
  1111. <Chip
  1112. size="small"
  1113. label={t(bomDescription)}
  1114. variant="outlined"
  1115. sx={chipSx}
  1116. />
  1117. ) : null}
  1118. {bomType ? (
  1119. <Chip
  1120. size="small"
  1121. label={t(bomType)}
  1122. color="info"
  1123. variant="outlined"
  1124. sx={chipSx}
  1125. />
  1126. ) : null}
  1127. <Chip
  1128. size="small"
  1129. label={chipLabel}
  1130. color={statusColor as any}
  1131. sx={chipSx}
  1132. />
  1133. </Stack>
  1134. <Stack
  1135. spacing={0.35}
  1136. sx={{ mt: 0.75, color: "text.secondary" }}
  1137. >
  1138. <Typography variant="body2">
  1139. {t("Required Qty")}: {process.requiredQty} (
  1140. {process.uom})
  1141. </Typography>
  1142. <Typography variant="body2">
  1143. {t("Production date")}:{" "}
  1144. {process.date
  1145. ? dayjs(process.date as any).format(
  1146. OUTPUT_DATE_FORMAT,
  1147. )
  1148. : "-"}
  1149. </Typography>
  1150. <Typography variant="body2">
  1151. {t("Assume Time Need")}:{" "}
  1152. {process.timeNeedToComplete} {t("minutes")}
  1153. </Typography>
  1154. </Stack>
  1155. <Stack
  1156. direction="row"
  1157. flexWrap="wrap"
  1158. useFlexGap
  1159. spacing={1}
  1160. sx={{ mt: "auto", pt: 1.5 }}
  1161. >
  1162. <Button
  1163. variant="contained"
  1164. size="small"
  1165. onClick={() =>
  1166. onSelectProcess(process.jobOrderId, process.id)
  1167. }
  1168. >
  1169. {t("View Details")}
  1170. </Button>
  1171. <Button
  1172. variant="contained"
  1173. size="small"
  1174. disabled={
  1175. process.assignedTo != null ||
  1176. process.matchStatus == "completed" ||
  1177. process.pickOrderStatus != "completed"
  1178. }
  1179. onClick={() =>
  1180. handleAssignPickOrder(
  1181. process.pickOrderId,
  1182. process.jobOrderId,
  1183. process.id,
  1184. )
  1185. }
  1186. >
  1187. {t("Matching Stock")}
  1188. </Button>
  1189. {statusLower !== "completed" && (
  1190. <Button
  1191. variant="contained"
  1192. size="small"
  1193. disabled={!canManageUpdateJo}
  1194. onClick={() =>
  1195. canManageUpdateJo
  1196. ? openConfirm(
  1197. t("Confirm to update this Job Order?"),
  1198. async () => {
  1199. await handleUpdateJo(process);
  1200. },
  1201. )
  1202. : undefined
  1203. }
  1204. >
  1205. {t("Update Job Order")}
  1206. </Button>
  1207. )}
  1208. {canQc && (
  1209. <Button
  1210. variant="contained"
  1211. size="small"
  1212. onClick={() => handleViewStockIn(process)}
  1213. >
  1214. {t("view stockin")}
  1215. </Button>
  1216. )}
  1217. </Stack>
  1218. <Typography
  1219. variant="caption"
  1220. color="text.secondary"
  1221. sx={{ mt: 1.25 }}
  1222. >
  1223. {jobOrderCode}
  1224. {" · "}
  1225. {t("Lot No")}: {process.lotNo ?? "-"}
  1226. </Typography>
  1227. </Box>
  1228. </Card>
  1229. </Grid>
  1230. );
  1231. })}
  1232. </Grid>
  1233. </Box>
  1234. ))}
  1235. </Box>
  1236. <IconButton
  1237. aria-label={t("Next page")}
  1238. onClick={goNextPage}
  1239. disabled={
  1240. safePage + 1 >= totalPages || filteredProcesses.length === 0
  1241. }
  1242. sx={{ alignSelf: "center" }}
  1243. >
  1244. <ChevronRight />
  1245. </IconButton>
  1246. </Box>
  1247. <QcStockInModal
  1248. session={sessionToken}
  1249. open={openModal}
  1250. onClose={closeNewModal}
  1251. inputDetail={modalInfo}
  1252. printerCombo={printerCombo}
  1253. warehouse={[]}
  1254. printSource="productionProcess"
  1255. uiMode="default"
  1256. />
  1257. <Dialog open={confirmOpen} onClose={closeConfirm} maxWidth="xs" fullWidth>
  1258. <DialogTitle>{t("Confirm")}</DialogTitle>
  1259. <DialogContent>
  1260. <Typography variant="body2">{confirmMessage}</Typography>
  1261. </DialogContent>
  1262. <DialogActions>
  1263. <Button onClick={closeConfirm} disabled={confirmLoading}>
  1264. {t("Cancel")}
  1265. </Button>
  1266. <Button
  1267. variant="contained"
  1268. onClick={onConfirm}
  1269. disabled={confirmLoading || !pendingConfirmAction}
  1270. >
  1271. {confirmLoading ? t("Processing...") : t("Confirm")}
  1272. </Button>
  1273. </DialogActions>
  1274. </Dialog>
  1275. {filteredProcesses.length > 0 && (
  1276. <Typography
  1277. variant="body2"
  1278. color="text.secondary"
  1279. align="center"
  1280. sx={{ mt: 2 }}
  1281. >
  1282. {safePage + 1} / {totalPages}
  1283. </Typography>
  1284. )}
  1285. </Box>
  1286. )}
  1287. </Box>
  1288. );
  1289. };
  1290. export default ProductProcessList;