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.
 
 

667 lines
22 KiB

  1. "use client";
  2. import {
  3. Box,
  4. Button,
  5. Stack,
  6. TextField,
  7. Typography,
  8. Alert,
  9. CircularProgress,
  10. Table,
  11. TableBody,
  12. TableCell,
  13. TableContainer,
  14. TableHead,
  15. TableRow,
  16. Paper,
  17. TablePagination,
  18. Modal,
  19. Card,
  20. CardContent,
  21. CardActions,
  22. Chip,
  23. Accordion,
  24. AccordionSummary,
  25. AccordionDetails,
  26. } from "@mui/material";
  27. import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
  28. import { useCallback, useEffect, useState, useRef, useMemo } from "react";
  29. import { useTranslation } from "react-i18next";
  30. import { useRouter } from "next/navigation";
  31. import {
  32. fetchALLPickOrderLineLotDetails,
  33. updateStockOutLineStatus,
  34. createStockOutLine,
  35. recordPickExecutionIssue,
  36. fetchFGPickOrders,
  37. FGPickOrderResponse,
  38. autoAssignAndReleasePickOrder,
  39. AutoAssignReleaseResponse,
  40. checkPickOrderCompletion,
  41. PickOrderCompletionResponse,
  42. checkAndCompletePickOrderByConsoCode,
  43. fetchCompletedDoPickOrders,
  44. CompletedDoPickOrderResponse,
  45. CompletedDoPickOrderSearchParams,
  46. fetchLotDetailsByPickOrderId
  47. } from "@/app/api/pickOrder/actions";
  48. import { fetchNameList, NameList } from "@/app/api/user/actions";
  49. import {
  50. FormProvider,
  51. useForm,
  52. } from "react-hook-form";
  53. import SearchBox, { Criterion } from "../SearchBox";
  54. import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
  55. import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
  56. import QrCodeIcon from '@mui/icons-material/QrCode';
  57. import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
  58. import { useSession } from "next-auth/react";
  59. import { SessionWithTokens } from "@/config/authConfig";
  60. import { fetchStockInLineInfo } from "@/app/api/po/actions";
  61. import GoodPickExecutionForm from "./GoodPickExecutionForm";
  62. import FGPickOrderCard from "./FGPickOrderCard";
  63. import dayjs from "dayjs";
  64. import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
  65. import { printDN, printDNLabels } from "@/app/api/do/actions";
  66. import Swal from "sweetalert2";
  67. interface Props {
  68. filterArgs: Record<string, any>;
  69. }
  70. // ✅ 新增:Pick Order 数据接口
  71. interface PickOrderData {
  72. pickOrderId: number;
  73. pickOrderCode: string;
  74. pickOrderConsoCode: string;
  75. pickOrderStatus: string;
  76. completedDate: string;
  77. lots: any[];
  78. }
  79. const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {
  80. const { t } = useTranslation("pickOrder");
  81. const router = useRouter();
  82. const { data: session } = useSession() as { data: SessionWithTokens | null };
  83. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  84. // ✅ 新增:已完成 DO Pick Orders 状态
  85. const [completedDoPickOrders, setCompletedDoPickOrders] = useState<CompletedDoPickOrderResponse[]>([]);
  86. const [completedDoPickOrdersLoading, setCompletedDoPickOrdersLoading] = useState(false);
  87. // ✅ 新增:详情视图状态
  88. const [selectedDoPickOrder, setSelectedDoPickOrder] = useState<CompletedDoPickOrderResponse | null>(null);
  89. const [showDetailView, setShowDetailView] = useState(false);
  90. const [detailLotData, setDetailLotData] = useState<any[]>([]);
  91. // ✅ 新增:搜索状态
  92. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  93. const [filteredDoPickOrders, setFilteredDoPickOrders] = useState<CompletedDoPickOrderResponse[]>([]);
  94. // ✅ 新增:分页状态
  95. const [paginationController, setPaginationController] = useState({
  96. pageNum: 0,
  97. pageSize: 10,
  98. });
  99. const formProps = useForm();
  100. const errors = formProps.formState.errors;
  101. // ✅ Print handler functions
  102. const handleDN = useCallback(async (deliveryOrderId: number, pickOrderId: number) => {
  103. const askNumofCarton = await Swal.fire({
  104. title: t("Enter the number of cartons: "),
  105. icon: "info",
  106. input: "number",
  107. inputPlaceholder: t("Number of cartons"),
  108. inputAttributes:{
  109. min: "1",
  110. step: "1"
  111. },
  112. inputValidator: (value) => {
  113. if(!value){
  114. return t("You need to enter a number")
  115. }
  116. if(parseInt(value) < 1){
  117. return t("Number must be at least 1");
  118. }
  119. return null
  120. },
  121. showCancelButton: true,
  122. confirmButtonText: t("Confirm"),
  123. cancelButtonText: t("Cancel"),
  124. confirmButtonColor: "#8dba00",
  125. cancelButtonColor: "#F04438",
  126. showLoaderOnConfirm: true,
  127. allowOutsideClick: () => !Swal.isLoading()
  128. });
  129. if (askNumofCarton.isConfirmed) {
  130. const numOfCartons = askNumofCarton.value;
  131. try{
  132. const printRequest = {
  133. printerId: 1,
  134. printQty: 1,
  135. isDraft: false,
  136. numOfCarton: numOfCartons,
  137. deliveryOrderId: deliveryOrderId,
  138. pickOrderId: pickOrderId
  139. };
  140. console.log("Printing Delivery Note with request: ", printRequest);
  141. const response = await printDN(printRequest);
  142. console.log("Print Delivery Note response: ", response);
  143. if(response.success){
  144. Swal.fire({
  145. position: "bottom-end",
  146. icon: "success",
  147. text: t("Printed Successfully."),
  148. showConfirmButton: false,
  149. timer: 1500
  150. });
  151. } else {
  152. console.error("Print failed: ", response.message);
  153. }
  154. } catch(error){
  155. console.error("error: ", error)
  156. }
  157. }
  158. }, [t]);
  159. const handleDNandLabel = useCallback(async (deliveryOrderId: number, pickOrderId: number) => {
  160. const askNumofCarton = await Swal.fire({
  161. title: t("Enter the number of cartons: "),
  162. icon: "info",
  163. input: "number",
  164. inputPlaceholder: t("Number of cartons"),
  165. inputAttributes:{
  166. min: "1",
  167. step: "1"
  168. },
  169. inputValidator: (value) => {
  170. if(!value){
  171. return t("You need to enter a number")
  172. }
  173. if(parseInt(value) < 1){
  174. return t("Number must be at least 1");
  175. }
  176. return null
  177. },
  178. showCancelButton: true,
  179. confirmButtonText: t("Confirm"),
  180. cancelButtonText: t("Cancel"),
  181. confirmButtonColor: "#8dba00",
  182. cancelButtonColor: "#F04438",
  183. showLoaderOnConfirm: true,
  184. allowOutsideClick: () => !Swal.isLoading()
  185. });
  186. if (askNumofCarton.isConfirmed) {
  187. const numOfCartons = askNumofCarton.value;
  188. try{
  189. const printDNRequest = {
  190. printerId: 1,
  191. printQty: 1,
  192. isDraft: false,
  193. numOfCarton: numOfCartons,
  194. deliveryOrderId: deliveryOrderId,
  195. pickOrderId: pickOrderId
  196. };
  197. const printDNLabelsRequest = {
  198. printerId: 1,
  199. printQty: 1,
  200. numOfCarton: numOfCartons,
  201. deliveryOrderId: deliveryOrderId
  202. };
  203. console.log("Printing Labels with request: ", printDNLabelsRequest);
  204. console.log("Printing DN with request: ", printDNRequest);
  205. const LabelsResponse = await printDNLabels(printDNLabelsRequest);
  206. const DNResponse = await printDN(printDNRequest);
  207. console.log("Print Labels response: ", LabelsResponse);
  208. console.log("Print DN response: ", DNResponse);
  209. if(LabelsResponse.success && DNResponse.success){
  210. Swal.fire({
  211. position: "bottom-end",
  212. icon: "success",
  213. text: t("Printed Successfully."),
  214. showConfirmButton: false,
  215. timer: 1500
  216. });
  217. } else {
  218. if(!LabelsResponse.success){
  219. console.error("Print failed: ", LabelsResponse.message);
  220. }
  221. else{
  222. console.error("Print failed: ", DNResponse.message);
  223. }
  224. }
  225. } catch(error){
  226. console.error("error: ", error)
  227. }
  228. }
  229. }, [t]);
  230. const handleLabel = useCallback(async (deliveryOrderId: number) => {
  231. const askNumofCarton = await Swal.fire({
  232. title: t("Enter the number of cartons: "),
  233. icon: "info",
  234. input: "number",
  235. inputPlaceholder: t("Number of cartons"),
  236. inputAttributes:{
  237. min: "1",
  238. step: "1"
  239. },
  240. inputValidator: (value) => {
  241. if(!value){
  242. return t("You need to enter a number")
  243. }
  244. if(parseInt(value) < 1){
  245. return t("Number must be at least 1");
  246. }
  247. return null
  248. },
  249. showCancelButton: true,
  250. confirmButtonText: t("Confirm"),
  251. cancelButtonText: t("Cancel"),
  252. confirmButtonColor: "#8dba00",
  253. cancelButtonColor: "#F04438",
  254. showLoaderOnConfirm: true,
  255. allowOutsideClick: () => !Swal.isLoading()
  256. });
  257. if (askNumofCarton.isConfirmed) {
  258. const numOfCartons = askNumofCarton.value;
  259. try{
  260. const printRequest = {
  261. printerId: 1,
  262. printQty: 1,
  263. numOfCarton: numOfCartons,
  264. deliveryOrderId: deliveryOrderId,
  265. };
  266. console.log("Printing Labels with request: ", printRequest);
  267. const response = await printDNLabels(printRequest);
  268. console.log("Print Labels response: ", response);
  269. if(response.success){
  270. Swal.fire({
  271. position: "bottom-end",
  272. icon: "success",
  273. text: t("Printed Successfully."),
  274. showConfirmButton: false,
  275. timer: 1500
  276. });
  277. } else {
  278. console.error("Print failed: ", response.message);
  279. }
  280. } catch(error){
  281. console.error("error: ", error)
  282. }
  283. }
  284. }, [t]);
  285. // ✅ 修改:使用新的 API 获取已完成的 DO Pick Orders
  286. const fetchCompletedDoPickOrdersData = useCallback(async (searchParams?: CompletedDoPickOrderSearchParams) => {
  287. if (!currentUserId) return;
  288. setCompletedDoPickOrdersLoading(true);
  289. try {
  290. console.log("🔍 Fetching completed DO pick orders with params:", searchParams);
  291. const completedDoPickOrders = await fetchCompletedDoPickOrders(currentUserId, searchParams);
  292. setCompletedDoPickOrders(completedDoPickOrders);
  293. setFilteredDoPickOrders(completedDoPickOrders);
  294. console.log("✅ Fetched completed DO pick orders:", completedDoPickOrders);
  295. } catch (error) {
  296. console.error("❌ Error fetching completed DO pick orders:", error);
  297. setCompletedDoPickOrders([]);
  298. setFilteredDoPickOrders([]);
  299. } finally {
  300. setCompletedDoPickOrdersLoading(false);
  301. }
  302. }, [currentUserId]);
  303. // ✅ 初始化时获取数据
  304. useEffect(() => {
  305. if (currentUserId) {
  306. fetchCompletedDoPickOrdersData();
  307. }
  308. }, [currentUserId, fetchCompletedDoPickOrdersData]);
  309. // ✅ 修改:搜索功能使用新的 API
  310. const handleSearch = useCallback((query: Record<string, any>) => {
  311. setSearchQuery({ ...query });
  312. console.log("Search query:", query);
  313. const searchParams: CompletedDoPickOrderSearchParams = {
  314. pickOrderCode: query.pickOrderCode || undefined,
  315. shopName: query.shopName || undefined,
  316. deliveryNo: query.deliveryNo || undefined,
  317. //ticketNo: query.ticketNo || undefined,
  318. };
  319. // 使用新的 API 进行搜索
  320. fetchCompletedDoPickOrdersData(searchParams);
  321. }, [fetchCompletedDoPickOrdersData]);
  322. // ✅ 修复:重命名函数避免重复声明
  323. const handleSearchReset = useCallback(() => {
  324. setSearchQuery({});
  325. fetchCompletedDoPickOrdersData(); // 重新获取所有数据
  326. }, [fetchCompletedDoPickOrdersData]);
  327. // ✅ 分页功能
  328. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  329. setPaginationController(prev => ({
  330. ...prev,
  331. pageNum: newPage,
  332. }));
  333. }, []);
  334. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  335. const newPageSize = parseInt(event.target.value, 10);
  336. setPaginationController({
  337. pageNum: 0,
  338. pageSize: newPageSize,
  339. });
  340. }, []);
  341. // ✅ 分页数据
  342. const paginatedData = useMemo(() => {
  343. const startIndex = paginationController.pageNum * paginationController.pageSize;
  344. const endIndex = startIndex + paginationController.pageSize;
  345. return filteredDoPickOrders.slice(startIndex, endIndex);
  346. }, [filteredDoPickOrders, paginationController]);
  347. // ✅ 搜索条件
  348. const searchCriteria: Criterion<any>[] = [
  349. {
  350. label: t("Pick Order Code"),
  351. paramName: "pickOrderCode",
  352. type: "text",
  353. },
  354. {
  355. label: t("Shop Name"),
  356. paramName: "shopName",
  357. type: "text",
  358. },
  359. {
  360. label: t("Delivery No"),
  361. paramName: "deliveryNo",
  362. type: "text",
  363. }
  364. ];
  365. const handleDetailClick = useCallback(async (doPickOrder: CompletedDoPickOrderResponse) => {
  366. setSelectedDoPickOrder(doPickOrder);
  367. setShowDetailView(true);
  368. // ✅ 修复:使用新的 API 根据 pickOrderId 获取 lot 详情
  369. try {
  370. const lotDetails = await fetchLotDetailsByPickOrderId(doPickOrder.pickOrderId);
  371. setDetailLotData(lotDetails);
  372. console.log("✅ Loaded detail lot data for pick order:", doPickOrder.pickOrderCode, lotDetails);
  373. // ✅ 触发打印按钮状态更新 - 基于详情数据
  374. const allCompleted = lotDetails.length > 0 && lotDetails.every(lot =>
  375. lot.processingStatus === 'completed'
  376. );
  377. // ✅ 发送事件,包含标签页信息
  378. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  379. detail: {
  380. allLotsCompleted: allCompleted,
  381. tabIndex: 2 // ✅ 明确指定这是来自标签页 2 的事件
  382. }
  383. }));
  384. } catch (error) {
  385. console.error("❌ Error loading detail lot data:", error);
  386. setDetailLotData([]);
  387. // ✅ 如果加载失败,禁用打印按钮
  388. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  389. detail: {
  390. allLotsCompleted: false,
  391. tabIndex: 2
  392. }
  393. }));
  394. }
  395. }, []);
  396. // ✅ 返回列表视图
  397. const handleBackToList = useCallback(() => {
  398. setShowDetailView(false);
  399. setSelectedDoPickOrder(null);
  400. setDetailLotData([]);
  401. // ✅ 返回列表时禁用打印按钮
  402. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  403. detail: {
  404. allLotsCompleted: false,
  405. tabIndex: 2
  406. }
  407. }));
  408. }, []);
  409. // ✅ 如果显示详情视图,渲染类似 GoodPickExecution 的表格
  410. if (showDetailView && selectedDoPickOrder) {
  411. return (
  412. <FormProvider {...formProps}>
  413. <Box>
  414. {/* 返回按钮和标题 */}
  415. <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
  416. <Button variant="outlined" onClick={handleBackToList}>
  417. {t("Back to List")}
  418. </Button>
  419. <Typography variant="h6">
  420. {t("Pick Order Details")}: {selectedDoPickOrder.pickOrderCode}
  421. </Typography>
  422. </Box>
  423. {/* 订单基本信息 */}
  424. <Box sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5', borderRadius: 1 }}>
  425. <Typography variant="h6" gutterBottom>
  426. {t("Order Information")}
  427. </Typography>
  428. <Typography variant="body2">
  429. <strong>{t("Shop Name")}:</strong> {selectedDoPickOrder.shopName}
  430. </Typography>
  431. <Typography variant="body2">
  432. <strong>{t("Delivery No")}:</strong> {selectedDoPickOrder.deliveryNo}
  433. </Typography>
  434. <Typography variant="body2">
  435. <strong>{t("Completed Date")}:</strong> {dayjs(selectedDoPickOrder.completedDate).format(OUTPUT_DATE_FORMAT)}
  436. </Typography>
  437. </Box>
  438. {/* ✅ 添加数据检查 */}
  439. {detailLotData.length === 0 ? (
  440. <Box sx={{ p: 3, textAlign: 'center' }}>
  441. <Typography variant="body2" color="text.secondary">
  442. {t("No lot details found for this order")}
  443. </Typography>
  444. </Box>
  445. ) : (
  446. /* 显示完成数据的表格 */
  447. <TableContainer component={Paper}>
  448. <Table>
  449. <TableHead>
  450. <TableRow>
  451. <TableCell>{t("Pick Order Code")}</TableCell>
  452. <TableCell>{t("Item Code")}</TableCell>
  453. <TableCell>{t("Item Name")}</TableCell>
  454. <TableCell>{t("Lot No")}</TableCell>
  455. <TableCell>{t("Location")}</TableCell>
  456. <TableCell>{t("Required Qty")}</TableCell>
  457. <TableCell>{t("Actual Pick Qty")}</TableCell>
  458. <TableCell>{t("Submitted Status")}</TableCell>
  459. </TableRow>
  460. </TableHead>
  461. <TableBody>
  462. {detailLotData.map((lot, index) => (
  463. <TableRow key={index}>
  464. <TableCell>{lot.pickOrderCode || 'N/A'}</TableCell>
  465. <TableCell>{lot.itemCode || 'N/A'}</TableCell>
  466. <TableCell>{lot.itemName || 'N/A'}</TableCell>
  467. <TableCell>{lot.lotNo || 'N/A'}</TableCell>
  468. <TableCell>{lot.location || 'N/A'}</TableCell>
  469. <TableCell>{lot.requiredQty || 0}</TableCell>
  470. <TableCell>{lot.actualPickQty || 0}</TableCell>
  471. <TableCell>
  472. <Chip
  473. label={t(lot.processingStatus || 'unknown')}
  474. color={lot.processingStatus === 'completed' ? 'success' : 'default'}
  475. size="small"
  476. />
  477. </TableCell>
  478. </TableRow>
  479. ))}
  480. </TableBody>
  481. </Table>
  482. </TableContainer>
  483. )}
  484. </Box>
  485. </FormProvider>
  486. );
  487. }
  488. // ✅ 默认列表视图
  489. return (
  490. <FormProvider {...formProps}>
  491. <Box>
  492. {/* 搜索框 */}
  493. <Box sx={{ mb: 2 }}>
  494. <SearchBox
  495. criteria={searchCriteria}
  496. onSearch={handleSearch}
  497. onReset={handleSearchReset}
  498. />
  499. </Box>
  500. {/* 加载状态 */}
  501. {completedDoPickOrdersLoading ? (
  502. <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
  503. <CircularProgress />
  504. </Box>
  505. ) : (
  506. <Box>
  507. {/* 结果统计 */}
  508. <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
  509. {t("Completed DO pick orders: ")} {filteredDoPickOrders.length}
  510. </Typography>
  511. {/* 列表 */}
  512. {filteredDoPickOrders.length === 0 ? (
  513. <Box sx={{ p: 3, textAlign: 'center' }}>
  514. <Typography variant="body2" color="text.secondary">
  515. {t("No completed DO pick orders found")}
  516. </Typography>
  517. </Box>
  518. ) : (
  519. <Stack spacing={2}>
  520. {paginatedData.map((doPickOrder) => (
  521. <Card key={doPickOrder.id}>
  522. <CardContent>
  523. <Stack direction="row" justifyContent="space-between" alignItems="center">
  524. <Box>
  525. <Typography variant="h6">
  526. {doPickOrder.pickOrderCode}
  527. </Typography>
  528. <Typography variant="body2" color="text.secondary">
  529. {doPickOrder.shopName} - {doPickOrder.deliveryNo}
  530. </Typography>
  531. <Typography variant="body2" color="text.secondary">
  532. {t("Completed")}: {dayjs(doPickOrder.completedDate).format(OUTPUT_DATE_FORMAT)}
  533. </Typography>
  534. </Box>
  535. <Box>
  536. <Chip
  537. label={t(doPickOrder.pickOrderStatus)}
  538. color={doPickOrder.pickOrderStatus === 'completed' ? 'success' : 'default'}
  539. size="small"
  540. sx={{ mb: 1 }}
  541. />
  542. <Typography variant="body2" color="text.secondary">
  543. {doPickOrder.fgPickOrders.length} {t("FG orders")}
  544. </Typography>
  545. </Box>
  546. </Stack>
  547. </CardContent>
  548. <CardActions>
  549. <Button
  550. variant="outlined"
  551. onClick={() => handleDetailClick(doPickOrder)}
  552. >
  553. {t("View Details")}
  554. </Button>
  555. {doPickOrder.fgPickOrders && doPickOrder.fgPickOrders.length > 0 && (
  556. <>
  557. <Button
  558. variant="contained"
  559. onClick={() => handleDN(
  560. doPickOrder.fgPickOrders[0].deliveryOrderId,
  561. doPickOrder.fgPickOrders[0].pickOrderId
  562. )}
  563. >
  564. {t("Print Pick Order")}
  565. </Button>
  566. <Button
  567. variant="contained"
  568. onClick={() => handleDNandLabel(
  569. doPickOrder.fgPickOrders[0].deliveryOrderId,
  570. doPickOrder.fgPickOrders[0].pickOrderId
  571. )}
  572. >
  573. {t("Print DN & Label")}
  574. </Button>
  575. <Button
  576. variant="contained"
  577. onClick={() => handleLabel(
  578. doPickOrder.fgPickOrders[0].deliveryOrderId
  579. )}
  580. >
  581. {t("Print Label")}
  582. </Button>
  583. </>
  584. )}
  585. </CardActions>
  586. </Card>
  587. ))}
  588. </Stack>
  589. )}
  590. {/* 分页 */}
  591. {filteredDoPickOrders.length > 0 && (
  592. <TablePagination
  593. component="div"
  594. count={filteredDoPickOrders.length}
  595. page={paginationController.pageNum}
  596. rowsPerPage={paginationController.pageSize}
  597. onPageChange={handlePageChange}
  598. onRowsPerPageChange={handlePageSizeChange}
  599. rowsPerPageOptions={[5, 10, 25, 50]}
  600. />
  601. )}
  602. </Box>
  603. )}
  604. </Box>
  605. </FormProvider>
  606. );
  607. };
  608. export default GoodPickExecutionRecord;