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.
 
 

558 lines
21 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. Checkbox, // Add Checkbox import
  27. } from "@mui/material";
  28. import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
  29. import { useCallback, useEffect, useState, useRef, useMemo } from "react";
  30. import { useTranslation } from "react-i18next";
  31. import { useRouter } from "next/navigation";
  32. import {
  33. fetchCompletedJobOrderPickOrdersWithCompletedSecondScan,
  34. fetchCompletedJobOrderPickOrderLotDetails
  35. } from "@/app/api/jo/actions";
  36. import { fetchNameList, NameList } from "@/app/api/user/actions";
  37. import {
  38. FormProvider,
  39. useForm,
  40. } from "react-hook-form";
  41. import SearchBox, { Criterion } from "../SearchBox";
  42. import { useSession } from "next-auth/react";
  43. import { SessionWithTokens } from "@/config/authConfig";
  44. import { PrinterCombo } from "@/app/api/settings/printer";
  45. interface Props {
  46. filterArgs: Record<string, any>;
  47. }
  48. // 修改:已完成的 Job Order Pick Order 接口
  49. interface CompletedJobOrderPickOrder {
  50. id: number;
  51. pickOrderId: number;
  52. pickOrderCode: string;
  53. pickOrderConsoCode: string;
  54. pickOrderTargetDate: string;
  55. pickOrderStatus: string;
  56. completedDate: string;
  57. jobOrderId: number;
  58. jobOrderCode: string;
  59. jobOrderName: string;
  60. reqQty: number;
  61. uom: string;
  62. planStart: string;
  63. planEnd: string;
  64. secondScanCompleted: boolean;
  65. totalItems: number;
  66. completedItems: number;
  67. }
  68. // 新增:Lot 详情接口
  69. interface LotDetail {
  70. lotId: number;
  71. lotNo: string;
  72. expiryDate: string;
  73. location: string;
  74. availableQty: number;
  75. requiredQty: number;
  76. actualPickQty: number;
  77. processingStatus: string;
  78. lotAvailability: string;
  79. pickOrderId: number;
  80. pickOrderCode: string;
  81. pickOrderConsoCode: string;
  82. pickOrderLineId: number;
  83. stockOutLineId: number;
  84. stockOutLineStatus: string;
  85. routerIndex: number;
  86. routerArea: string;
  87. routerRoute: string;
  88. uomShortDesc: string;
  89. secondQrScanStatus: string;
  90. itemId: number;
  91. itemCode: string;
  92. itemName: string;
  93. uomCode: string;
  94. uomDesc: string;
  95. }
  96. const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
  97. const { t } = useTranslation("jo");
  98. const router = useRouter();
  99. const { data: session } = useSession() as { data: SessionWithTokens | null };
  100. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  101. // 修改:已完成 Job Order Pick Orders 状态
  102. const [completedJobOrderPickOrders, setCompletedJobOrderPickOrders] = useState<CompletedJobOrderPickOrder[]>([]);
  103. const [completedJobOrderPickOrdersLoading, setCompletedJobOrderPickOrdersLoading] = useState(false);
  104. // 修改:详情视图状态
  105. const [selectedJobOrderPickOrder, setSelectedJobOrderPickOrder] = useState<CompletedJobOrderPickOrder | null>(null);
  106. const [showDetailView, setShowDetailView] = useState(false);
  107. const [detailLotData, setDetailLotData] = useState<LotDetail[]>([]);
  108. const [detailLotDataLoading, setDetailLotDataLoading] = useState(false);
  109. // 修改:搜索状态
  110. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  111. const [filteredJobOrderPickOrders, setFilteredJobOrderPickOrders] = useState<CompletedJobOrderPickOrder[]>([]);
  112. // 修改:分页状态
  113. const [paginationController, setPaginationController] = useState({
  114. pageNum: 0,
  115. pageSize: 10,
  116. });
  117. const formProps = useForm();
  118. const errors = formProps.formState.errors;
  119. // 修改:使用新的 Job Order API 获取已完成的 Job Order Pick Orders
  120. const fetchCompletedJobOrderPickOrdersData = useCallback(async () => {
  121. if (!currentUserId) return;
  122. setCompletedJobOrderPickOrdersLoading(true);
  123. try {
  124. console.log("🔍 Fetching completed Job Order pick orders...");
  125. const completedJobOrderPickOrders = await fetchCompletedJobOrderPickOrdersWithCompletedSecondScan(currentUserId);
  126. setCompletedJobOrderPickOrders(completedJobOrderPickOrders);
  127. setFilteredJobOrderPickOrders(completedJobOrderPickOrders);
  128. console.log(" Fetched completed Job Order pick orders:", completedJobOrderPickOrders);
  129. } catch (error) {
  130. console.error("❌ Error fetching completed Job Order pick orders:", error);
  131. setCompletedJobOrderPickOrders([]);
  132. setFilteredJobOrderPickOrders([]);
  133. } finally {
  134. setCompletedJobOrderPickOrdersLoading(false);
  135. }
  136. }, [currentUserId]);
  137. // 新增:获取 lot 详情数据
  138. const fetchLotDetailsData = useCallback(async (pickOrderId: number) => {
  139. setDetailLotDataLoading(true);
  140. try {
  141. console.log("🔍 Fetching lot details for pick order:", pickOrderId);
  142. const lotDetails = await fetchCompletedJobOrderPickOrderLotDetails(pickOrderId);
  143. setDetailLotData(lotDetails);
  144. console.log(" Fetched lot details:", lotDetails);
  145. } catch (error) {
  146. console.error("❌ Error fetching lot details:", error);
  147. setDetailLotData([]);
  148. } finally {
  149. setDetailLotDataLoading(false);
  150. }
  151. }, []);
  152. // 修改:初始化时获取数据
  153. useEffect(() => {
  154. if (currentUserId) {
  155. fetchCompletedJobOrderPickOrdersData();
  156. }
  157. }, [currentUserId, fetchCompletedJobOrderPickOrdersData]);
  158. // 修改:搜索功能
  159. const handleSearch = useCallback((query: Record<string, any>) => {
  160. setSearchQuery({ ...query });
  161. console.log("Search query:", query);
  162. const filtered = completedJobOrderPickOrders.filter((pickOrder) => {
  163. const pickOrderCodeMatch = !query.pickOrderCode ||
  164. pickOrder.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
  165. const jobOrderCodeMatch = !query.jobOrderCode ||
  166. pickOrder.jobOrderCode?.toLowerCase().includes((query.jobOrderCode || "").toLowerCase());
  167. const jobOrderNameMatch = !query.jobOrderName ||
  168. pickOrder.jobOrderName?.toLowerCase().includes((query.jobOrderName || "").toLowerCase());
  169. return pickOrderCodeMatch && jobOrderCodeMatch && jobOrderNameMatch;
  170. });
  171. setFilteredJobOrderPickOrders(filtered);
  172. console.log("Filtered Job Order pick orders count:", filtered.length);
  173. }, [completedJobOrderPickOrders]);
  174. // 修改:重置搜索
  175. const handleSearchReset = useCallback(() => {
  176. setSearchQuery({});
  177. setFilteredJobOrderPickOrders(completedJobOrderPickOrders);
  178. }, [completedJobOrderPickOrders]);
  179. // 修改:分页功能
  180. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  181. setPaginationController(prev => ({
  182. ...prev,
  183. pageNum: newPage,
  184. }));
  185. }, []);
  186. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  187. const newPageSize = parseInt(event.target.value, 10);
  188. setPaginationController({
  189. pageNum: 0,
  190. pageSize: newPageSize,
  191. });
  192. }, []);
  193. // 修改:分页数据
  194. const paginatedData = useMemo(() => {
  195. const startIndex = paginationController.pageNum * paginationController.pageSize;
  196. const endIndex = startIndex + paginationController.pageSize;
  197. return filteredJobOrderPickOrders.slice(startIndex, endIndex);
  198. }, [filteredJobOrderPickOrders, paginationController]);
  199. // 修改:搜索条件
  200. const searchCriteria: Criterion<any>[] = [
  201. {
  202. label: t("Pick Order Code"),
  203. paramName: "pickOrderCode",
  204. type: "text",
  205. },
  206. {
  207. label: t("Job Order Code"),
  208. paramName: "jobOrderCode",
  209. type: "text",
  210. },
  211. {
  212. label: t("Job Order Item Name"),
  213. paramName: "jobOrderName",
  214. type: "text",
  215. }
  216. ];
  217. // 修改:详情点击处理
  218. const handleDetailClick = useCallback(async (jobOrderPickOrder: CompletedJobOrderPickOrder) => {
  219. setSelectedJobOrderPickOrder(jobOrderPickOrder);
  220. setShowDetailView(true);
  221. // 获取 lot 详情数据
  222. await fetchLotDetailsData(jobOrderPickOrder.pickOrderId);
  223. // 触发打印按钮状态更新 - 基于详情数据
  224. const allCompleted = jobOrderPickOrder.secondScanCompleted;
  225. // 发送事件,包含标签页信息
  226. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  227. detail: {
  228. allLotsCompleted: allCompleted,
  229. tabIndex: 2 // 明确指定这是来自标签页 2 的事件
  230. }
  231. }));
  232. }, [fetchLotDetailsData]);
  233. // 修改:返回列表视图
  234. const handleBackToList = useCallback(() => {
  235. setShowDetailView(false);
  236. setSelectedJobOrderPickOrder(null);
  237. setDetailLotData([]);
  238. // 返回列表时禁用打印按钮
  239. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  240. detail: {
  241. allLotsCompleted: false,
  242. tabIndex: 2
  243. }
  244. }));
  245. }, []);
  246. // 修改:如果显示详情视图,渲染 Job Order 详情和 Lot 信息
  247. if (showDetailView && selectedJobOrderPickOrder) {
  248. return (
  249. <FormProvider {...formProps}>
  250. <Box>
  251. {/* 返回按钮和标题 */}
  252. <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
  253. <Button variant="outlined" onClick={handleBackToList}>
  254. {t("Back to List")}
  255. </Button>
  256. <Typography variant="h6">
  257. {t("Job Order Pick Order Details")}: {selectedJobOrderPickOrder.pickOrderCode}
  258. </Typography>
  259. </Box>
  260. {/* Job Order 信息卡片 */}
  261. <Card sx={{ mb: 2 }}>
  262. <CardContent>
  263. <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
  264. <Typography variant="subtitle1">
  265. <strong>{t("Pick Order Code")}:</strong> {selectedJobOrderPickOrder.pickOrderCode}
  266. </Typography>
  267. <Typography variant="subtitle1">
  268. <strong>{t("Job Order Code")}:</strong> {selectedJobOrderPickOrder.jobOrderCode}
  269. </Typography>
  270. <Typography variant="subtitle1">
  271. <strong>{t("Job Order Item Name")}:</strong> {selectedJobOrderPickOrder.jobOrderName}
  272. </Typography>
  273. <Typography variant="subtitle1">
  274. <strong>{t("Target Date")}:</strong> {selectedJobOrderPickOrder.pickOrderTargetDate}
  275. </Typography>
  276. </Stack>
  277. <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap" sx={{ mt: 2 }}>
  278. <Typography variant="subtitle1">
  279. <strong>{t("Required Qty")}:</strong> {selectedJobOrderPickOrder.reqQty} {selectedJobOrderPickOrder.uom}
  280. </Typography>
  281. </Stack>
  282. </CardContent>
  283. </Card>
  284. {/* 修改:Lot 详情表格 - 添加复选框列 */}
  285. <Card>
  286. <CardContent>
  287. <Typography variant="h6" gutterBottom>
  288. {t("Lot Details")}
  289. </Typography>
  290. {detailLotDataLoading ? (
  291. <Box sx={{ display: 'flex', justifyContent: 'center', p: 2 }}>
  292. <CircularProgress />
  293. </Box>
  294. ) : (
  295. <TableContainer component={Paper}>
  296. <Table>
  297. <TableHead>
  298. <TableRow>
  299. <TableCell>{t("Index")}</TableCell>
  300. <TableCell>{t("Location")}</TableCell>
  301. <TableCell>{t("Item Code")}</TableCell>
  302. <TableCell>{t("Item Name")}</TableCell>
  303. <TableCell>{t("Lot No")}</TableCell>
  304. <TableCell align="right">{t("Required Qty")}</TableCell>
  305. <TableCell align="right">{t("Actual Pick Qty")}</TableCell>
  306. <TableCell align="center">{t("Processing Status")}</TableCell>
  307. <TableCell align="center">{t("Second Scan Status")}</TableCell>
  308. </TableRow>
  309. </TableHead>
  310. <TableBody>
  311. {detailLotData.length === 0 ? (
  312. <TableRow>
  313. <TableCell colSpan={10} align="center"> {/* 恢复原来的 colSpan */}
  314. <Typography variant="body2" color="text.secondary">
  315. {t("No lot details available")}
  316. </Typography>
  317. </TableCell>
  318. </TableRow>
  319. ) : (
  320. detailLotData.map((lot, index) => (
  321. <TableRow key={lot.lotId}>
  322. <TableCell>
  323. <Typography variant="body2" fontWeight="bold">
  324. {index + 1}
  325. </Typography>
  326. </TableCell>
  327. <TableCell>
  328. <Typography variant="body2">
  329. {lot.routerRoute || '-'}
  330. </Typography>
  331. </TableCell>
  332. <TableCell>{lot.itemCode}</TableCell>
  333. <TableCell>{lot.itemName}</TableCell>
  334. <TableCell>{lot.lotNo}</TableCell>
  335. <TableCell align="right">
  336. {lot.requiredQty?.toLocaleString() || 0} ({lot.uomShortDesc})
  337. </TableCell>
  338. <TableCell align="right">
  339. {lot.actualPickQty?.toLocaleString() || 0} ({lot.uomShortDesc})
  340. </TableCell>
  341. {/* 修改:Processing Status 使用复选框 */}
  342. <TableCell align="center">
  343. <Box sx={{
  344. display: 'flex',
  345. justifyContent: 'center',
  346. alignItems: 'center',
  347. width: '100%',
  348. height: '100%'
  349. }}>
  350. <Checkbox
  351. checked={lot.processingStatus === 'completed'}
  352. disabled={true}
  353. readOnly={true}
  354. size="large"
  355. sx={{
  356. color: lot.processingStatus === 'completed' ? 'success.main' : 'grey.400',
  357. '&.Mui-checked': {
  358. color: 'success.main',
  359. },
  360. transform: 'scale(1.3)',
  361. '& .MuiSvgIcon-root': {
  362. fontSize: '1.5rem',
  363. }
  364. }}
  365. />
  366. </Box>
  367. </TableCell>
  368. {/* 修改:Second Scan Status 使用复选框 */}
  369. <TableCell align="center">
  370. <Box sx={{
  371. display: 'flex',
  372. justifyContent: 'center',
  373. alignItems: 'center',
  374. width: '100%',
  375. height: '100%'
  376. }}>
  377. <Checkbox
  378. checked={lot.secondQrScanStatus === 'completed'}
  379. disabled={true}
  380. readOnly={true}
  381. size="large"
  382. sx={{
  383. color: lot.secondQrScanStatus === 'completed' ? 'success.main' : 'grey.400',
  384. '&.Mui-checked': {
  385. color: 'success.main',
  386. },
  387. transform: 'scale(1.3)',
  388. '& .MuiSvgIcon-root': {
  389. fontSize: '1.5rem',
  390. }
  391. }}
  392. />
  393. </Box>
  394. </TableCell>
  395. </TableRow>
  396. ))
  397. )}
  398. </TableBody>
  399. </Table>
  400. </TableContainer>
  401. )}
  402. </CardContent>
  403. </Card>
  404. </Box>
  405. </FormProvider>
  406. );
  407. }
  408. // 修改:默认列表视图
  409. return (
  410. <FormProvider {...formProps}>
  411. <Box>
  412. {/* 搜索框 */}
  413. <Box sx={{ mb: 2 }}>
  414. <SearchBox
  415. criteria={searchCriteria}
  416. onSearch={handleSearch}
  417. onReset={handleSearchReset}
  418. />
  419. </Box>
  420. {/* 加载状态 */}
  421. {completedJobOrderPickOrdersLoading ? (
  422. <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
  423. <CircularProgress />
  424. </Box>
  425. ) : (
  426. <Box>
  427. {/* 结果统计 */}
  428. <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
  429. {t("Total")}: {filteredJobOrderPickOrders.length} {t("completed Job Order pick orders with matching")}
  430. </Typography>
  431. {/* 列表 */}
  432. {filteredJobOrderPickOrders.length === 0 ? (
  433. <Box sx={{ p: 3, textAlign: 'center' }}>
  434. <Typography variant="body2" color="text.secondary">
  435. {t("No completed Job Order pick orders with matching found")}
  436. </Typography>
  437. </Box>
  438. ) : (
  439. <Stack spacing={2}>
  440. {paginatedData.map((jobOrderPickOrder) => (
  441. <Card key={jobOrderPickOrder.id}>
  442. <CardContent>
  443. <Stack direction="row" justifyContent="space-between" alignItems="center">
  444. <Box>
  445. <Typography variant="h6">
  446. {jobOrderPickOrder.pickOrderCode}
  447. </Typography>
  448. <Typography variant="body2" color="text.secondary">
  449. {jobOrderPickOrder.jobOrderName} - {jobOrderPickOrder.jobOrderCode}
  450. </Typography>
  451. <Typography variant="body2" color="text.secondary">
  452. {t("Completed")}: {new Date(jobOrderPickOrder.completedDate).toLocaleString()}
  453. </Typography>
  454. <Typography variant="body2" color="text.secondary">
  455. {t("Target Date")}: {jobOrderPickOrder.pickOrderTargetDate}
  456. </Typography>
  457. </Box>
  458. <Box>
  459. <Chip
  460. label={jobOrderPickOrder.pickOrderStatus}
  461. color={jobOrderPickOrder.pickOrderStatus === 'completed' ? 'success' : 'default'}
  462. size="small"
  463. sx={{ mb: 1 }}
  464. />
  465. {/*
  466. <Typography variant="body2" color="text.secondary">
  467. {jobOrderPickOrder.completedItems}/{jobOrderPickOrder.totalItems} {t("items completed")}
  468. </Typography>
  469. */}
  470. <Chip
  471. label={jobOrderPickOrder.secondScanCompleted ? t("Second Scan Completed") : t("Second Scan Pending")}
  472. color={jobOrderPickOrder.secondScanCompleted ? 'success' : 'warning'}
  473. size="small"
  474. sx={{ mt: 1 }}
  475. />
  476. </Box>
  477. </Stack>
  478. </CardContent>
  479. <CardActions>
  480. <Button
  481. variant="outlined"
  482. onClick={() => handleDetailClick(jobOrderPickOrder)}
  483. >
  484. {t("View Details")}
  485. </Button>
  486. </CardActions>
  487. </Card>
  488. ))}
  489. </Stack>
  490. )}
  491. {/* 分页 */}
  492. {filteredJobOrderPickOrders.length > 0 && (
  493. <TablePagination
  494. component="div"
  495. count={filteredJobOrderPickOrders.length}
  496. page={paginationController.pageNum}
  497. rowsPerPage={paginationController.pageSize}
  498. onPageChange={handlePageChange}
  499. onRowsPerPageChange={handlePageSizeChange}
  500. rowsPerPageOptions={[5, 10, 25, 50]}
  501. />
  502. )}
  503. </Box>
  504. )}
  505. </Box>
  506. </FormProvider>
  507. );
  508. };
  509. export default FInishedJobOrderRecord;