FPSMS-frontend
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 

522 líneas
17 KiB

  1. "use client";
  2. import { PickOrderResult } from "@/app/api/pickOrder";
  3. import { useCallback, useEffect, useMemo, useState } from "react";
  4. import { useTranslation } from "react-i18next";
  5. import SearchBox, { Criterion } from "../SearchBox";
  6. import {
  7. flatten,
  8. intersectionWith,
  9. isEmpty,
  10. sortBy,
  11. uniqBy,
  12. upperCase,
  13. upperFirst,
  14. } from "lodash";
  15. import {
  16. arrayToDayjs,
  17. } from "@/app/utils/formatUtil";
  18. import JoPickOrderList from "@/components/JoWorkbench/JoPickOrderList";
  19. import { Button, Grid, Stack, Tab, Tabs, TabsProps, Typography, Box, TextField, Autocomplete } from "@mui/material";
  20. import Jodetail from "./Jodetail"
  21. import PickExecution from "./JobPickExecution";
  22. import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions";
  23. import { fetchPickOrderClient, autoAssignAndReleasePickOrder, autoAssignAndReleasePickOrderByStore } from "@/app/api/pickOrder/actions";
  24. import { useSession } from "next-auth/react";
  25. import { SessionWithTokens } from "@/config/authConfig";
  26. import JobPickExecutionsecondscan from "./JobPickExecutionsecondscan";
  27. import FInishedJobOrderRecord from "./FInishedJobOrderRecord";
  28. import JobPickExecution from "./JobPickExecution";
  29. import CompleteJobOrderRecord from "./completeJobOrderRecord";
  30. //import JoPickOrderList from "./JoPickOrderList";
  31. import {
  32. // fetchUnassignedJobOrderPickOrders,
  33. assignJobOrderPickOrder,
  34. fetchJobOrderLotsHierarchical,
  35. fetchCompletedJobOrderPickOrders,
  36. fetchCompletedJobOrderPickOrderRecords
  37. } from "@/app/api/jo/actions";
  38. import { fetchPrinterCombo } from "@/app/api/settings/printer";
  39. import { PrinterCombo } from "@/app/api/settings/printer";
  40. import JoPickOrderDetail from "./JoPickOrderDetail";
  41. import MaterialPickStatusTable from "./MaterialPickStatusTable";
  42. interface Props {
  43. //pickOrders: PickOrderResult[];
  44. printerCombo: PrinterCombo[];
  45. }
  46. type SearchQuery = Partial<
  47. Omit<PickOrderResult, "id" | "consoCode" | "completeDate">
  48. >;
  49. type SearchParamNames = keyof SearchQuery;
  50. const JodetailSearch: React.FC<Props> = ({ printerCombo }) => {
  51. const { t } = useTranslation("jo");
  52. const { data: session } = useSession() as { data: SessionWithTokens | null };
  53. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  54. const [isOpenCreateModal, setIsOpenCreateModal] = useState(false)
  55. const [items, setItems] = useState<ItemCombo[]>([])
  56. const [printButtonsEnabled, setPrintButtonsEnabled] = useState(false);
  57. //const [filteredPickOrders, setFilteredPickOrders] = useState(pickOrders);
  58. const [filterArgs, setFilterArgs] = useState<Record<string, any>>({});
  59. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  60. const [tabIndex, setTabIndex] = useState(0);
  61. const [totalCount, setTotalCount] = useState<number>();
  62. const [isAssigning, setIsAssigning] = useState(false);
  63. const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]);
  64. const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
  65. const [hasAssignedJobOrders, setHasAssignedJobOrders] = useState(false);
  66. const [hasDataTab0, setHasDataTab0] = useState(false);
  67. const [hasDataTab1, setHasDataTab1] = useState(false);
  68. const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
  69. // Add printer selection state
  70. const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | null>(
  71. printerCombo && printerCombo.length > 0 ? printerCombo[0] : null
  72. );
  73. const [printQty, setPrintQty] = useState<number>(1);
  74. const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState<boolean>(
  75. typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true'
  76. );
  77. useEffect(() => {
  78. const onJobOrderDataStatus = (e: CustomEvent) => {
  79. const { hasData, tabIndex: idx } = e.detail || {};
  80. if (idx === 0) setHasDataTab0(!!hasData);
  81. if (idx === 1) setHasDataTab1(!!hasData);
  82. };
  83. window.addEventListener('jobOrderDataStatus', onJobOrderDataStatus as EventListener);
  84. return () => window.removeEventListener('jobOrderDataStatus', onJobOrderDataStatus as EventListener);
  85. }, []);
  86. useEffect(() => {
  87. const handleJobOrderDataChange = (event: CustomEvent) => {
  88. const { hasData, tabIndex: eventTabIndex } = event.detail;
  89. // Update the state based on which tab has data
  90. if (eventTabIndex === 0 || eventTabIndex === 1) {
  91. setHasAssignedJobOrders(hasData);
  92. console.log(`Job order data status for tab ${eventTabIndex}:`, hasData);
  93. }
  94. };
  95. window.addEventListener('jobOrderDataStatus', handleJobOrderDataChange as EventListener);
  96. return () => {
  97. window.removeEventListener('jobOrderDataStatus', handleJobOrderDataChange as EventListener);
  98. };
  99. }, []);
  100. useEffect(() => {
  101. const onAssigned = () => {
  102. localStorage.removeItem('hideCompletedUntilNext');
  103. setHideCompletedUntilNext(false);
  104. };
  105. window.addEventListener('pickOrderAssigned', onAssigned);
  106. return () => window.removeEventListener('pickOrderAssigned', onAssigned);
  107. }, []);
  108. useEffect(() => {
  109. const handleCompletionStatusChange = (event: CustomEvent) => {
  110. const { allLotsCompleted, tabIndex: eventTabIndex } = event.detail;
  111. // 修复:根据标签页和事件来源决定是否更新打印按钮状态
  112. if (eventTabIndex === undefined || eventTabIndex === tabIndex) {
  113. setPrintButtonsEnabled(allLotsCompleted);
  114. console.log(`Print buttons enabled for tab ${tabIndex}:`, allLotsCompleted);
  115. }
  116. };
  117. window.addEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener);
  118. return () => {
  119. window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener);
  120. };
  121. }, [tabIndex]);
  122. // 新增:处理标签页切换时的打印按钮状态重置
  123. useEffect(() => {
  124. // 当切换到标签页 2 (GoodPickExecutionRecord) 时,重置打印按钮状态
  125. if (tabIndex === 2) {
  126. setPrintButtonsEnabled(false);
  127. console.log("Reset print buttons for Pick Execution Record tab");
  128. }
  129. }, [tabIndex]);
  130. const handleAssignByStore = async (storeId: "2/F" | "4/F") => {
  131. if (!currentUserId) {
  132. console.error("Missing user id in session");
  133. return;
  134. }
  135. setIsAssigning(true);
  136. try {
  137. const res = await autoAssignAndReleasePickOrderByStore(currentUserId, storeId);
  138. console.log("Assign by store result:", res);
  139. // Handle different response codes
  140. if (res.code === "SUCCESS") {
  141. console.log(" Successfully assigned pick order to store", storeId);
  142. // Trigger refresh to show newly assigned data
  143. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  144. } else if (res.code === "USER_BUSY") {
  145. console.warn("⚠️ User already has pick orders in progress:", res.message);
  146. // Show warning but still refresh to show existing orders
  147. alert(`Warning: ${res.message}`);
  148. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  149. } else if (res.code === "NO_ORDERS") {
  150. console.log("ℹ️ No available pick orders for store", storeId);
  151. alert(`Info: ${res.message}`);
  152. } else {
  153. console.log("ℹ️ Assignment result:", res.message);
  154. alert(`Info: ${res.message}`);
  155. }
  156. } catch (error) {
  157. console.error("❌ Error assigning by store:", error);
  158. alert("Error occurred during assignment");
  159. } finally {
  160. setIsAssigning(false);
  161. }
  162. };
  163. // Manual assignment handler - uses the action function
  164. /*
  165. const loadUnassignedOrders = useCallback(async () => {
  166. setIsLoadingUnassigned(true);
  167. try {
  168. const orders = await fetchUnassignedJobOrderPickOrders();
  169. setUnassignedOrders(orders);
  170. } catch (error) {
  171. console.error("Error loading unassigned orders:", error);
  172. setUnassignedOrders([]);
  173. } finally {
  174. setIsLoadingUnassigned(false);
  175. }
  176. }, []);
  177. */
  178. // 分配订单给当前用户
  179. const handleAssignOrder = useCallback(async (pickOrderId: number) => {
  180. if (!currentUserId) {
  181. console.error("Missing user id in session");
  182. return;
  183. }
  184. try {
  185. const result = await assignJobOrderPickOrder(pickOrderId, currentUserId);
  186. if (result.message === "Successfully assigned") {
  187. console.log(" Successfully assigned pick order");
  188. // 刷新数据
  189. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  190. // 重新加载未分配订单列表
  191. // loadUnassignedOrders();
  192. } else {
  193. console.warn("⚠️ Assignment failed:", result.message);
  194. alert(`Assignment failed: ${result.message}`);
  195. }
  196. } catch (error) {
  197. console.error("❌ Error assigning order:", error);
  198. alert("Error occurred during assignment");
  199. }
  200. }, [currentUserId]);
  201. // 在组件加载时获取未分配订单
  202. /*
  203. useEffect(() => {
  204. if (tabIndex === 0) {
  205. loadUnassignedOrders();
  206. }
  207. }, [tabIndex, loadUnassignedOrders]);
  208. */
  209. const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
  210. (_e, newValue) => {
  211. setTabIndex(newValue);
  212. },
  213. [],
  214. );
  215. const openCreateModal = useCallback(async () => {
  216. console.log("testing")
  217. const res = await fetchAllItemsInClient()
  218. console.log(res)
  219. setItems(res)
  220. setIsOpenCreateModal(true)
  221. }, [])
  222. const closeCreateModal = useCallback(() => {
  223. setIsOpenCreateModal(false)
  224. }, [])
  225. useEffect(() => {
  226. if (tabIndex === 3) {
  227. const loadItems = async () => {
  228. try {
  229. const itemsData = await fetchAllItemsInClient();
  230. console.log("PickOrderSearch loaded items:", itemsData.length);
  231. setItems(itemsData);
  232. } catch (error) {
  233. console.error("Error loading items in PickOrderSearch:", error);
  234. }
  235. };
  236. // 如果还没有数据,则加载
  237. if (items.length === 0) {
  238. loadItems();
  239. }
  240. }
  241. }, [tabIndex, items.length]);
  242. useEffect(() => {
  243. const handleCompletionStatusChange = (event: CustomEvent) => {
  244. const { allLotsCompleted } = event.detail;
  245. setPrintButtonsEnabled(allLotsCompleted);
  246. console.log("Print buttons enabled:", allLotsCompleted);
  247. };
  248. window.addEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener);
  249. return () => {
  250. window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener);
  251. };
  252. }, []);
  253. /*
  254. const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
  255. () => {
  256. const baseCriteria: Criterion<SearchParamNames>[] = [
  257. {
  258. label: tabIndex === 3 ? t("Item Code") : t("Code"),
  259. paramName: "code",
  260. type: "text"
  261. },
  262. {
  263. label: t("Type"),
  264. paramName: "type",
  265. type: "autocomplete",
  266. options: tabIndex === 3
  267. ?
  268. [
  269. { value: "Consumable", label: t("Consumable") },
  270. { value: "Material", label: t("Material") },
  271. { value: "Product", label: t("Product") }
  272. ]
  273. :
  274. sortBy(
  275. uniqBy(
  276. pickOrders.map((po) => ({
  277. value: po.type,
  278. label: t(upperCase(po.type)),
  279. })),
  280. "value",
  281. ),
  282. "label",
  283. ),
  284. },
  285. ];
  286. // Add Job Order search for Create Item tab (tabIndex === 3)
  287. if (tabIndex === 3) {
  288. baseCriteria.splice(1, 0, {
  289. label: t("Job Order"),
  290. paramName: "jobOrderCode" as any, // Type assertion for now
  291. type: "text",
  292. });
  293. baseCriteria.splice(2, 0, {
  294. label: t("Target Date"),
  295. paramName: "targetDate",
  296. type: "date",
  297. });
  298. } else {
  299. baseCriteria.splice(1, 0, {
  300. label: t("Target Date From"),
  301. label2: t("Target Date To"),
  302. paramName: "targetDate",
  303. type: "dateRange",
  304. });
  305. }
  306. // Add Items/Item Name criteria
  307. baseCriteria.push({
  308. label: tabIndex === 3 ? t("Item Name") : t("Items"),
  309. paramName: "items",
  310. type: tabIndex === 3 ? "text" : "autocomplete",
  311. options: tabIndex === 3
  312. ? []
  313. :
  314. uniqBy(
  315. flatten(
  316. sortBy(
  317. pickOrders.map((po) =>
  318. po.items
  319. ? po.items.map((item) => ({
  320. value: item.name,
  321. label: item.name,
  322. }))
  323. : [],
  324. ),
  325. "label",
  326. ),
  327. ),
  328. "value",
  329. ),
  330. });
  331. // Add Status criteria for non-Create Item tabs
  332. if (tabIndex !== 3) {
  333. baseCriteria.push({
  334. label: t("Status"),
  335. paramName: "status",
  336. type: "autocomplete",
  337. options: sortBy(
  338. uniqBy(
  339. pickOrders.map((po) => ({
  340. value: po.status,
  341. label: t(upperFirst(po.status)),
  342. })),
  343. "value",
  344. ),
  345. "label",
  346. ),
  347. });
  348. }
  349. return baseCriteria;
  350. },
  351. [pickOrders, t, tabIndex, items],
  352. );
  353. */
  354. const handleSwitchToRecordTab = useCallback(() => {
  355. setTabIndex(1); // 切换到 CompleteJobOrderRecord 标签页(tabIndex 1)
  356. }, []);
  357. /*
  358. const fetchNewPagePickOrder = useCallback(
  359. async (
  360. pagingController: Record<string, number>,
  361. filterArgs: Record<string, number>,
  362. ) => {
  363. const params = {
  364. ...pagingController,
  365. ...filterArgs,
  366. };
  367. const res = await fetchPickOrderClient(params);
  368. if (res) {
  369. console.log(res);
  370. setFilteredPickOrders(res.records);
  371. setTotalCount(res.total);
  372. }
  373. },
  374. [],
  375. );
  376. const onReset = useCallback(() => {
  377. setFilteredPickOrders(pickOrders);
  378. }, [pickOrders]);
  379. */
  380. /*
  381. useEffect(() => {
  382. if (!isOpenCreateModal) {
  383. setTabIndex(1)
  384. setTimeout(async () => {
  385. setTabIndex(0)
  386. }, 200)
  387. }
  388. }, [isOpenCreateModal])
  389. */
  390. // 添加处理提料单创建成功的函数
  391. const handlePickOrderCreated = useCallback(() => {
  392. // 切换到 Assign & Release 标签页 (tabIndex = 1)
  393. setTabIndex(2);
  394. }, []);
  395. return (
  396. <Box sx={{
  397. height: '100vh',
  398. overflow: 'auto'
  399. }}>
  400. {/* Header section with printer selection */}
  401. <Box sx={{
  402. p: 1,
  403. borderBottom: '1px solid #e0e0e0',
  404. minHeight: 'auto',
  405. display: 'flex',
  406. alignItems: 'center',
  407. justifyContent: 'space-between',
  408. gap: 2,
  409. flexWrap: 'wrap',
  410. }}>
  411. {/* Left side - Title */}
  412. {/* Right side - Printer selection (only show on tab 1) */}
  413. {tabIndex === 1 && (
  414. <Stack
  415. direction="row"
  416. spacing={2}
  417. sx={{
  418. alignItems: 'center',
  419. flexWrap: 'wrap',
  420. rowGap: 1,
  421. }}
  422. >
  423. <Typography variant="body2" sx={{ minWidth: 'fit-content', mr: 1.5 }}>
  424. {t("Select Printer")}:
  425. </Typography>
  426. <Autocomplete
  427. options={(printerCombo || []).filter(printer => printer.type === 'A4')}
  428. getOptionLabel={(option) =>
  429. option.name || option.label || option.code || `Printer ${option.id}`
  430. }
  431. value={selectedPrinter}
  432. onChange={(_, newValue) => setSelectedPrinter(newValue)}
  433. sx={{ minWidth: 200 }}
  434. size="small"
  435. renderInput={(params) => (
  436. <TextField {...params} placeholder={t("Printer")}inputProps={{ ...params.inputProps, readOnly: true }} />
  437. )}
  438. />
  439. <Typography variant="body2" sx={{ minWidth: 'fit-content', ml: 1 }}>
  440. {t("Print Quantity")}:
  441. </Typography>
  442. <TextField
  443. type="number"
  444. label={t("Print Quantity")}
  445. value={printQty}
  446. onChange={(e) => {
  447. const value = parseInt(e.target.value) || 1;
  448. setPrintQty(Math.max(1, value));
  449. }}
  450. inputProps={{ min: 1, step: 1 }}
  451. sx={{ width: 120 }}
  452. size="small"
  453. />
  454. </Stack>
  455. )}
  456. </Box>
  457. {/* Tabs section */}
  458. <Box sx={{
  459. borderBottom: '1px solid #e0e0e0'
  460. }}>
  461. <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
  462. <Tab label={t("Jo Pick Order Detail")} iconPosition="end" />
  463. <Tab label={t("Complete Job Order Record")} iconPosition="end" />
  464. <Tab label={t("Material Pick Status")} iconPosition="end" />
  465. </Tabs>
  466. </Box>
  467. {/* Content section */}
  468. <Box sx={{ p: 2 }}>
  469. {tabIndex === 0 && <JoPickOrderList onSwitchToRecordTab={handleSwitchToRecordTab} printerCombo={printerCombo} />}
  470. {tabIndex === 1 && (
  471. <CompleteJobOrderRecord
  472. filterArgs={filterArgs}
  473. printerCombo={printerCombo}
  474. selectedPrinter={selectedPrinter}
  475. printQty={printQty}
  476. />
  477. )}
  478. {tabIndex === 2 && <MaterialPickStatusTable />}
  479. </Box>
  480. </Box>
  481. );
  482. };
  483. export default JodetailSearch;