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

FinishedGoodSearch.tsx 17 KiB

3ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
3ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
3ヶ月前
2ヶ月前
3ヶ月前
3ヶ月前
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  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 PickOrders from "./FinishedGood";
  20. import ConsolidatedPickOrders from "./ConsolidatedPickOrders";
  21. import PickExecution from "./GoodPickExecution";
  22. import CreatePickOrderModal from "./CreatePickOrderModal";
  23. import NewCreateItem from "./newcreatitem";
  24. import AssignAndRelease from "./AssignAndRelease";
  25. import AssignTo from "./assignTo";
  26. import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions";
  27. import { fetchPickOrderClient, autoAssignAndReleasePickOrder, autoAssignAndReleasePickOrderByStore, FGPickOrderResponse, fetchFGPickOrders } from "@/app/api/pickOrder/actions";
  28. import Jobcreatitem from "./Jobcreatitem";
  29. import { useSession } from "next-auth/react";
  30. import { SessionWithTokens } from "@/config/authConfig";
  31. import PickExecutionDetail from "./GoodPickExecutiondetail";
  32. import GoodPickExecutionRecord from "./GoodPickExecutionRecord";
  33. import Swal from "sweetalert2";
  34. import { PrintDeliveryNoteRequest, printDN } from "@/app/api/do/actions";
  35. interface Props {
  36. pickOrders: PickOrderResult[];
  37. }
  38. type SearchQuery = Partial<
  39. Omit<PickOrderResult, "id" | "consoCode" | "completeDate">
  40. >;
  41. type SearchParamNames = keyof SearchQuery;
  42. const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
  43. const { t } = useTranslation("pickOrder");
  44. const { data: session } = useSession() as { data: SessionWithTokens | null };
  45. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  46. const [isOpenCreateModal, setIsOpenCreateModal] = useState(false)
  47. const [items, setItems] = useState<ItemCombo[]>([])
  48. const [printButtonsEnabled, setPrintButtonsEnabled] = useState(false);
  49. const [filteredPickOrders, setFilteredPickOrders] = useState(pickOrders);
  50. const [filterArgs, setFilterArgs] = useState<Record<string, any>>({});
  51. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  52. const [tabIndex, setTabIndex] = useState(0);
  53. const [totalCount, setTotalCount] = useState<number>();
  54. const [isAssigning, setIsAssigning] = useState(false);
  55. const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState<boolean>(
  56. typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true'
  57. );
  58. useEffect(() => {
  59. const onAssigned = () => {
  60. localStorage.removeItem('hideCompletedUntilNext');
  61. setHideCompletedUntilNext(false);
  62. };
  63. window.addEventListener('pickOrderAssigned', onAssigned);
  64. return () => window.removeEventListener('pickOrderAssigned', onAssigned);
  65. }, []);
  66. const [fgPickOrdersData, setFgPickOrdersData] = useState<FGPickOrderResponse[]>([]);
  67. const handleAssignByStore = async (storeId: "2/F" | "4/F") => {
  68. if (!currentUserId) {
  69. console.error("Missing user id in session");
  70. return;
  71. }
  72. setIsAssigning(true);
  73. try {
  74. const res = await autoAssignAndReleasePickOrderByStore(currentUserId, storeId);
  75. console.log("Assign by store result:", res);
  76. // ✅ Handle different response codes
  77. if (res.code === "SUCCESS") {
  78. console.log("✅ Successfully assigned pick order to store", storeId);
  79. // ✅ Trigger refresh to show newly assigned data
  80. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  81. } else if (res.code === "USER_BUSY") {
  82. console.warn("⚠️ User already has pick orders in progress:", res.message);
  83. // ✅ Show warning but still refresh to show existing orders
  84. alert(`Warning: ${res.message}`);
  85. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  86. } else if (res.code === "NO_ORDERS") {
  87. console.log("ℹ️ No available pick orders for store", storeId);
  88. alert(`Info: ${res.message}`);
  89. } else {
  90. console.log("ℹ️ Assignment result:", res.message);
  91. alert(`Info: ${res.message}`);
  92. }
  93. } catch (error) {
  94. console.error("❌ Error assigning by store:", error);
  95. alert("Error occurred during assignment");
  96. } finally {
  97. setIsAssigning(false);
  98. }
  99. };
  100. // ✅ Manual assignment handler - uses the action function
  101. const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
  102. (_e, newValue) => {
  103. setTabIndex(newValue);
  104. },
  105. [],
  106. );
  107. const openCreateModal = useCallback(async () => {
  108. console.log("testing")
  109. const res = await fetchAllItemsInClient()
  110. console.log(res)
  111. setItems(res)
  112. setIsOpenCreateModal(true)
  113. }, [])
  114. const closeCreateModal = useCallback(() => {
  115. setIsOpenCreateModal(false)
  116. }, [])
  117. const handleDN = useCallback(async () =>{
  118. const askNumofCarton = await Swal.fire({
  119. title: t("Enter the number of cartons: "),
  120. input: "number",
  121. inputPlaceholder: t("Number of cartons"),
  122. inputAttributes:{
  123. min: "1",
  124. step: "1"
  125. },
  126. inputValidator: (value) => {
  127. if(!value){
  128. return t("You need to enter a number")
  129. }
  130. if(parseInt(value) < 1){
  131. return t("Number must be at least 1");
  132. }
  133. return null
  134. },
  135. showCancelButton: true,
  136. confirmButtonText: t("Confirm"),
  137. cancelButtonText: t("Cancel"),
  138. showLoaderOnConfirm: true,
  139. allowOutsideClick: () => !Swal.isLoading()
  140. });
  141. if (askNumofCarton.isConfirmed) {
  142. const numOfCartons = askNumofCarton.value;
  143. console.log(numOfCartons)
  144. }
  145. },[t]);
  146. const handleDNandLabel = useCallback(async () =>{
  147. const askNumofCarton = await Swal.fire({
  148. title: t("Enter the number of cartons: "),
  149. input: "number",
  150. inputPlaceholder: t("Number of cartons"),
  151. inputAttributes:{
  152. min: "1",
  153. step: "1"
  154. },
  155. inputValidator: (value) => {
  156. if(!value){
  157. return t("You need to enter a number")
  158. }
  159. if(parseInt(value) < 1){
  160. return t("Number must be at least 1");
  161. }
  162. return null
  163. },
  164. showCancelButton: true,
  165. confirmButtonText: t("Confirm"),
  166. cancelButtonText: t("Cancel"),
  167. showLoaderOnConfirm: true,
  168. allowOutsideClick: () => !Swal.isLoading()
  169. });
  170. if (askNumofCarton.isConfirmed) {
  171. const numOfCartons = askNumofCarton.value;
  172. }
  173. },[t]);
  174. const handleLabel = useCallback(async () =>{
  175. const askNumofCarton = await Swal.fire({
  176. title: t("Enter the number of cartons: "),
  177. input: "number",
  178. inputPlaceholder: t("Number of cartons"),
  179. inputAttributes:{
  180. min: "1",
  181. step: "1"
  182. },
  183. inputValidator: (value) => {
  184. if(!value){
  185. return t("You need to enter a number")
  186. }
  187. if(parseInt(value) < 1){
  188. return t("Number must be at least 1");
  189. }
  190. return null
  191. },
  192. showCancelButton: true,
  193. confirmButtonText: t("Confirm"),
  194. cancelButtonText: t("Cancel"),
  195. showLoaderOnConfirm: true,
  196. allowOutsideClick: () => !Swal.isLoading()
  197. });
  198. if (askNumofCarton.isConfirmed) {
  199. const numOfCartons = askNumofCarton.value;
  200. }
  201. },[t]);
  202. const handleDraft = useCallback(async () =>{
  203. },[t]);
  204. useEffect(() => {
  205. if (tabIndex === 3) {
  206. const loadItems = async () => {
  207. try {
  208. const itemsData = await fetchAllItemsInClient();
  209. console.log("PickOrderSearch loaded items:", itemsData.length);
  210. setItems(itemsData);
  211. } catch (error) {
  212. console.error("Error loading items in PickOrderSearch:", error);
  213. }
  214. };
  215. // 如果还没有数据,则加载
  216. if (items.length === 0) {
  217. loadItems();
  218. }
  219. }
  220. }, [tabIndex, items.length]);
  221. useEffect(() => {
  222. const handleCompletionStatusChange = (event: CustomEvent) => {
  223. const { allLotsCompleted } = event.detail;
  224. setPrintButtonsEnabled(allLotsCompleted);
  225. console.log("Print buttons enabled:", allLotsCompleted);
  226. };
  227. window.addEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener);
  228. return () => {
  229. window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener);
  230. };
  231. }, []);
  232. const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
  233. () => {
  234. const baseCriteria: Criterion<SearchParamNames>[] = [
  235. {
  236. label: tabIndex === 3 ? t("Item Code") : t("Code"),
  237. paramName: "code",
  238. type: "text"
  239. },
  240. {
  241. label: t("Type"),
  242. paramName: "type",
  243. type: "autocomplete",
  244. options: tabIndex === 3
  245. ?
  246. [
  247. { value: "Consumable", label: t("Consumable") },
  248. { value: "Material", label: t("Material") },
  249. { value: "Product", label: t("Product") }
  250. ]
  251. :
  252. sortBy(
  253. uniqBy(
  254. pickOrders.map((po) => ({
  255. value: po.type,
  256. label: t(upperCase(po.type)),
  257. })),
  258. "value",
  259. ),
  260. "label",
  261. ),
  262. },
  263. ];
  264. // Add Job Order search for Create Item tab (tabIndex === 3)
  265. if (tabIndex === 3) {
  266. baseCriteria.splice(1, 0, {
  267. label: t("Job Order"),
  268. paramName: "jobOrderCode" as any, // Type assertion for now
  269. type: "text",
  270. });
  271. baseCriteria.splice(2, 0, {
  272. label: t("Target Date"),
  273. paramName: "targetDate",
  274. type: "date",
  275. });
  276. } else {
  277. baseCriteria.splice(1, 0, {
  278. label: t("Target Date From"),
  279. label2: t("Target Date To"),
  280. paramName: "targetDate",
  281. type: "dateRange",
  282. });
  283. }
  284. // Add Items/Item Name criteria
  285. baseCriteria.push({
  286. label: tabIndex === 3 ? t("Item Name") : t("Items"),
  287. paramName: "items",
  288. type: tabIndex === 3 ? "text" : "autocomplete",
  289. options: tabIndex === 3
  290. ? []
  291. :
  292. uniqBy(
  293. flatten(
  294. sortBy(
  295. pickOrders.map((po) =>
  296. po.items
  297. ? po.items.map((item) => ({
  298. value: item.name,
  299. label: item.name,
  300. }))
  301. : [],
  302. ),
  303. "label",
  304. ),
  305. ),
  306. "value",
  307. ),
  308. });
  309. // Add Status criteria for non-Create Item tabs
  310. if (tabIndex !== 3) {
  311. baseCriteria.push({
  312. label: t("Status"),
  313. paramName: "status",
  314. type: "autocomplete",
  315. options: sortBy(
  316. uniqBy(
  317. pickOrders.map((po) => ({
  318. value: po.status,
  319. label: t(upperFirst(po.status)),
  320. })),
  321. "value",
  322. ),
  323. "label",
  324. ),
  325. });
  326. }
  327. return baseCriteria;
  328. },
  329. [pickOrders, t, tabIndex, items],
  330. );
  331. const fetchNewPagePickOrder = useCallback(
  332. async (
  333. pagingController: Record<string, number>,
  334. filterArgs: Record<string, number>,
  335. ) => {
  336. const params = {
  337. ...pagingController,
  338. ...filterArgs,
  339. };
  340. const res = await fetchPickOrderClient(params);
  341. if (res) {
  342. console.log(res);
  343. setFilteredPickOrders(res.records);
  344. setTotalCount(res.total);
  345. }
  346. },
  347. [],
  348. );
  349. const onReset = useCallback(() => {
  350. setFilteredPickOrders(pickOrders);
  351. }, [pickOrders]);
  352. useEffect(() => {
  353. if (!isOpenCreateModal) {
  354. setTabIndex(1)
  355. setTimeout(async () => {
  356. setTabIndex(0)
  357. }, 200)
  358. }
  359. }, [isOpenCreateModal])
  360. // 添加处理提料单创建成功的函数
  361. const handlePickOrderCreated = useCallback(() => {
  362. // 切换到 Assign & Release 标签页 (tabIndex = 1)
  363. setTabIndex(2);
  364. }, []);
  365. return (
  366. <Box sx={{
  367. height: '100vh', // Full viewport height
  368. overflow: 'auto' // Single scrollbar for the whole page
  369. }}>
  370. {/* Header section */}
  371. <Box sx={{ p: 2, borderBottom: '1px solid #e0e0e0' }}>
  372. <Stack rowGap={2}>
  373. <Grid container alignItems="center">
  374. <Grid item xs={8}>
  375. <Box mb={2}>
  376. <Typography variant="h4" marginInlineEnd={2}>
  377. {t("Finished Good Order")}
  378. </Typography>
  379. </Box>
  380. </Grid>
  381. </Grid>
  382. {/* First 4 buttons aligned left */}
  383. <Grid item xs={6}>
  384. <Stack direction="row" spacing={1}>
  385. <Button variant="contained" onClick={handleDraft}>{t("Print Draft")}</Button>
  386. <Button variant="contained" onClick={handleDNandLabel}>{t("Print Pick Order and DN Label")}</Button>
  387. <Button variant="contained" onClick={handleDN}>{t("Print Pick Order")}</Button>
  388. <Button variant="contained" onClick={handleLabel}>{t("Print DN Label")}</Button>
  389. </Stack>
  390. </Grid>
  391. {/* Last 2 buttons aligned right */}
  392. <Grid item xs={6} >
  393. <Stack direction="row" spacing={1}>
  394. <Button
  395. variant="contained"
  396. onClick={() => handleAssignByStore("2/F")}
  397. disabled={isAssigning}
  398. >
  399. {isAssigning ? t("Assigning pick order...") : t("Pick Execution 2/F")}
  400. </Button>
  401. <Button
  402. variant="contained"
  403. onClick={() => handleAssignByStore("4/F")}
  404. disabled={isAssigning}
  405. >
  406. {isAssigning ? t("Assigning pick order...") : t("Pick Execution 4/F")}
  407. </Button>
  408. </Stack>
  409. </Grid>
  410. {/* ✅ Updated print buttons with completion status */}
  411. <Grid item xs={6} display="flex" justifyContent="flex-end">
  412. <Stack direction="row" spacing={1}>
  413. {/*
  414. <Button
  415. variant={hideCompletedUntilNext ? "contained" : "outlined"}
  416. color={hideCompletedUntilNext ? "warning" : "inherit"}
  417. onClick={() => {
  418. const next = !hideCompletedUntilNext;
  419. setHideCompletedUntilNext(next);
  420. if (next) localStorage.setItem('hideCompletedUntilNext', 'true');
  421. else localStorage.removeItem('hideCompletedUntilNext');
  422. window.dispatchEvent(new Event('pickOrderAssigned')); // ask detail to re-fetch
  423. }}
  424. >
  425. {hideCompletedUntilNext ? t("Hide Completed: ON") : t("Hide Completed: OFF")}
  426. </Button>
  427. */}
  428. <Button
  429. variant="contained"
  430. disabled={!printButtonsEnabled}
  431. title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""}
  432. >
  433. {t("Print Draft")}
  434. </Button>
  435. <Button
  436. variant="contained"
  437. disabled={!printButtonsEnabled}
  438. title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""}
  439. >
  440. {t("Print Pick Order and DN Label")}
  441. </Button>
  442. <Button
  443. variant="contained"
  444. disabled={!printButtonsEnabled}
  445. title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""}
  446. >
  447. {t("Print Pick Order")}
  448. </Button>
  449. <Button
  450. variant="contained"
  451. disabled={!printButtonsEnabled}
  452. title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""}
  453. >
  454. {t("Print DN Label")}
  455. </Button>
  456. </Stack>
  457. </Grid>
  458. </Grid>
  459. </Stack>
  460. </Box>
  461. {/* Tabs section - ✅ Move the click handler here */}
  462. <Box sx={{
  463. borderBottom: '1px solid #e0e0e0'
  464. }}>
  465. <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
  466. <Tab label={t("Pick Order Detail")} iconPosition="end" />
  467. <Tab label={t("Pick Execution Detail")} iconPosition="end" />
  468. <Tab label={t("Pick Execution Record")} iconPosition="end" />
  469. </Tabs>
  470. </Box>
  471. {/* Content section - NO overflow: 'auto' here */}
  472. <Box sx={{
  473. p: 2
  474. }}>
  475. {tabIndex === 0 && <PickExecution filterArgs={filterArgs} />}
  476. {tabIndex === 1 && <PickExecutionDetail filterArgs={filterArgs} />}
  477. {tabIndex === 2 && <GoodPickExecutionRecord filterArgs={filterArgs} />}
  478. </Box>
  479. </Box>
  480. );
  481. };
  482. export default PickOrderSearch;