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

JodetailSearch.tsx 16 KiB

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