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

JodetailSearch.tsx 17 KiB

7ヶ月前
4ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
5ヶ月前
5ヶ月前
7ヶ月前
5ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
4ヶ月前
7ヶ月前
7ヶ月前
4ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
4ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
6ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
7ヶ月前
4ヶ月前
7ヶ月前
4ヶ月前
7ヶ月前
4ヶ月前
4ヶ月前
3ヶ月前
4ヶ月前
7ヶ月前
4ヶ月前
7ヶ月前
4ヶ月前
7ヶ月前
4ヶ月前
4ヶ月前
7ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  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;