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

JobPickExecutionsecondscan.tsx 48 KiB

2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
1ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411
  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. Checkbox,
  18. TablePagination,
  19. Modal,
  20. } from "@mui/material";
  21. import TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider';
  22. import { useCallback, useEffect, useState, useRef, useMemo } from "react";
  23. import { useTranslation } from "react-i18next";
  24. import { useRouter } from "next/navigation";
  25. // 修改:使用 Job Order API
  26. import {
  27. fetchCompletedJobOrderPickOrders,
  28. fetchUnassignedJobOrderPickOrders,
  29. assignJobOrderPickOrder,
  30. updateSecondQrScanStatus,
  31. submitSecondScanQuantity,
  32. recordSecondScanIssue
  33. } from "@/app/api/jo/actions";
  34. import { fetchNameList, NameList } from "@/app/api/user/actions";
  35. import {
  36. FormProvider,
  37. useForm,
  38. } from "react-hook-form";
  39. import SearchBox, { Criterion } from "../SearchBox";
  40. import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
  41. import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
  42. import QrCodeIcon from '@mui/icons-material/QrCode';
  43. import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
  44. import { useSession } from "next-auth/react";
  45. import { SessionWithTokens } from "@/config/authConfig";
  46. import { fetchStockInLineInfo } from "@/app/api/po/actions";
  47. import GoodPickExecutionForm from "./JobmatchForm";
  48. import FGPickOrderCard from "./FGPickOrderCard";
  49. interface Props {
  50. filterArgs: Record<string, any>;
  51. }
  52. // QR Code Modal Component (from GoodPickExecution)
  53. const QrCodeModal: React.FC<{
  54. open: boolean;
  55. onClose: () => void;
  56. lot: any | null;
  57. onQrCodeSubmit: (lotNo: string) => void;
  58. combinedLotData: any[];
  59. }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => {
  60. const { t } = useTranslation("jo");
  61. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  62. const [manualInput, setManualInput] = useState<string>('');
  63. const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false);
  64. const [manualInputError, setManualInputError] = useState<boolean>(false);
  65. const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false);
  66. const [qrScanFailed, setQrScanFailed] = useState<boolean>(false);
  67. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  68. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  69. const [scannedQrResult, setScannedQrResult] = useState<string>('');
  70. const { data: session } = useSession() as { data: SessionWithTokens | null };
  71. const currentUserId = session?.id ? parseInt(session.id) :
  72. // Process scanned QR codes
  73. useEffect(() => {
  74. if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) {
  75. const latestQr = qrValues[qrValues.length - 1];
  76. if (processedQrCodes.has(latestQr)) {
  77. console.log("QR code already processed, skipping...");
  78. return;
  79. }
  80. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  81. try {
  82. const qrData = JSON.parse(latestQr);
  83. if (qrData.stockInLineId && qrData.itemId) {
  84. setIsProcessingQr(true);
  85. setQrScanFailed(false);
  86. fetchStockInLineInfo(qrData.stockInLineId)
  87. .then((stockInLineInfo) => {
  88. console.log("Stock in line info:", stockInLineInfo);
  89. setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
  90. if (stockInLineInfo.lotNo === lot.lotNo) {
  91. console.log(` QR Code verified for lot: ${lot.lotNo}`);
  92. setQrScanSuccess(true);
  93. onQrCodeSubmit(lot.lotNo);
  94. onClose();
  95. resetScan();
  96. } else {
  97. console.log(`❌ QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`);
  98. setQrScanFailed(true);
  99. setManualInputError(true);
  100. setManualInputSubmitted(true);
  101. }
  102. })
  103. .catch((error) => {
  104. console.error("Error fetching stock in line info:", error);
  105. setScannedQrResult('Error fetching data');
  106. setQrScanFailed(true);
  107. setManualInputError(true);
  108. setManualInputSubmitted(true);
  109. })
  110. .finally(() => {
  111. setIsProcessingQr(false);
  112. });
  113. } else {
  114. const qrContent = latestQr.replace(/[{}]/g, '');
  115. setScannedQrResult(qrContent);
  116. if (qrContent === lot.lotNo) {
  117. setQrScanSuccess(true);
  118. onQrCodeSubmit(lot.lotNo);
  119. onClose();
  120. resetScan();
  121. } else {
  122. setQrScanFailed(true);
  123. setManualInputError(true);
  124. setManualInputSubmitted(true);
  125. }
  126. }
  127. } catch (error) {
  128. console.log("QR code is not JSON format, trying direct comparison");
  129. const qrContent = latestQr.replace(/[{}]/g, '');
  130. setScannedQrResult(qrContent);
  131. if (qrContent === lot.lotNo) {
  132. setQrScanSuccess(true);
  133. onQrCodeSubmit(lot.lotNo);
  134. onClose();
  135. resetScan();
  136. } else {
  137. setQrScanFailed(true);
  138. setManualInputError(true);
  139. setManualInputSubmitted(true);
  140. }
  141. }
  142. }
  143. }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes]);
  144. // Clear states when modal opens
  145. useEffect(() => {
  146. if (open) {
  147. setManualInput('');
  148. setManualInputSubmitted(false);
  149. setManualInputError(false);
  150. setIsProcessingQr(false);
  151. setQrScanFailed(false);
  152. setQrScanSuccess(false);
  153. setScannedQrResult('');
  154. setProcessedQrCodes(new Set());
  155. }
  156. }, [open]);
  157. useEffect(() => {
  158. if (lot) {
  159. setManualInput('');
  160. setManualInputSubmitted(false);
  161. setManualInputError(false);
  162. setIsProcessingQr(false);
  163. setQrScanFailed(false);
  164. setQrScanSuccess(false);
  165. setScannedQrResult('');
  166. setProcessedQrCodes(new Set());
  167. }
  168. }, [lot]);
  169. // Auto-submit manual input when it matches
  170. useEffect(() => {
  171. if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) {
  172. console.log(' Auto-submitting manual input:', manualInput.trim());
  173. const timer = setTimeout(() => {
  174. setQrScanSuccess(true);
  175. onQrCodeSubmit(lot.lotNo);
  176. onClose();
  177. setManualInput('');
  178. setManualInputError(false);
  179. setManualInputSubmitted(false);
  180. }, 200);
  181. return () => clearTimeout(timer);
  182. }
  183. }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]);
  184. const handleManualSubmit = () => {
  185. if (manualInput.trim() === lot?.lotNo) {
  186. setQrScanSuccess(true);
  187. onQrCodeSubmit(lot.lotNo);
  188. onClose();
  189. setManualInput('');
  190. } else {
  191. setQrScanFailed(true);
  192. setManualInputError(true);
  193. setManualInputSubmitted(true);
  194. }
  195. };
  196. useEffect(() => {
  197. if (open) {
  198. startScan();
  199. }
  200. }, [open, startScan]);
  201. return (
  202. <Modal open={open} onClose={onClose}>
  203. <Box sx={{
  204. position: 'absolute',
  205. top: '50%',
  206. left: '50%',
  207. transform: 'translate(-50%, -50%)',
  208. bgcolor: 'background.paper',
  209. p: 3,
  210. borderRadius: 2,
  211. minWidth: 400,
  212. }}>
  213. <Typography variant="h6" gutterBottom>
  214. {t("QR Code Scan for Lot")}: {lot?.lotNo}
  215. </Typography>
  216. {isProcessingQr && (
  217. <Box sx={{ mb: 2, p: 2, backgroundColor: '#e3f2fd', borderRadius: 1 }}>
  218. <Typography variant="body2" color="primary">
  219. {t("Processing QR code...")}
  220. </Typography>
  221. </Box>
  222. )}
  223. <Box sx={{ mb: 2 }}>
  224. <Typography variant="body2" gutterBottom>
  225. <strong>{t("Manual Input")}:</strong>
  226. </Typography>
  227. <TextField
  228. fullWidth
  229. size="small"
  230. value={manualInput}
  231. onChange={(e) => {
  232. setManualInput(e.target.value);
  233. if (qrScanFailed || manualInputError) {
  234. setQrScanFailed(false);
  235. setManualInputError(false);
  236. setManualInputSubmitted(false);
  237. }
  238. }}
  239. sx={{ mb: 1 }}
  240. error={manualInputSubmitted && manualInputError}
  241. helperText={
  242. manualInputSubmitted && manualInputError
  243. ? `${t("The input is not the same as the expected lot number.")}`
  244. : ''
  245. }
  246. />
  247. <Button
  248. variant="contained"
  249. onClick={handleManualSubmit}
  250. disabled={!manualInput.trim()}
  251. size="small"
  252. color="primary"
  253. >
  254. {t("Submit")}
  255. </Button>
  256. </Box>
  257. {qrValues.length > 0 && (
  258. <Box sx={{
  259. mb: 2,
  260. p: 2,
  261. backgroundColor: qrScanFailed ? '#ffebee' : qrScanSuccess ? '#e8f5e8' : '#f5f5f5',
  262. borderRadius: 1
  263. }}>
  264. <Typography variant="body2" color={qrScanFailed ? 'error' : qrScanSuccess ? 'success' : 'text.secondary'}>
  265. <strong>{t("QR Scan Result:")}</strong> {scannedQrResult}
  266. </Typography>
  267. {qrScanSuccess && (
  268. <Typography variant="caption" color="success" display="block">
  269. {t("Verified successfully!")}
  270. </Typography>
  271. )}
  272. </Box>
  273. )}
  274. <Box sx={{ mt: 2, textAlign: 'right' }}>
  275. <Button onClick={onClose} variant="outlined">
  276. {t("Cancel")}
  277. </Button>
  278. </Box>
  279. </Box>
  280. </Modal>
  281. );
  282. };
  283. const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
  284. const { t } = useTranslation("jo");
  285. const router = useRouter();
  286. const { data: session } = useSession() as { data: SessionWithTokens | null };
  287. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  288. // 修改:使用 Job Order 数据结构
  289. const [jobOrderData, setJobOrderData] = useState<any>(null);
  290. const [combinedLotData, setCombinedLotData] = useState<any[]>([]);
  291. const [combinedDataLoading, setCombinedDataLoading] = useState(false);
  292. const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]);
  293. // 添加未分配订单状态
  294. const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]);
  295. const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
  296. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  297. const [qrScanInput, setQrScanInput] = useState<string>('');
  298. const [qrScanError, setQrScanError] = useState<boolean>(false);
  299. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  300. const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({});
  301. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  302. const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false);
  303. const [paginationController, setPaginationController] = useState({
  304. pageNum: 0,
  305. pageSize: 10,
  306. });
  307. const [usernameList, setUsernameList] = useState<NameList[]>([]);
  308. const initializationRef = useRef(false);
  309. const autoAssignRef = useRef(false);
  310. const formProps = useForm();
  311. const errors = formProps.formState.errors;
  312. // Add QR modal states
  313. const [qrModalOpen, setQrModalOpen] = useState(false);
  314. const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);
  315. // Add GoodPickExecutionForm states
  316. const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
  317. const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
  318. // Add these missing state variables
  319. const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
  320. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  321. const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
  322. const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
  323. // 修改:加载未分配的 Job Order 订单
  324. const loadUnassignedOrders = useCallback(async () => {
  325. setIsLoadingUnassigned(true);
  326. try {
  327. const orders = await fetchUnassignedJobOrderPickOrders();
  328. setUnassignedOrders(orders);
  329. } catch (error) {
  330. console.error("Error loading unassigned orders:", error);
  331. } finally {
  332. setIsLoadingUnassigned(false);
  333. }
  334. }, []);
  335. // 修改:分配订单给当前用户
  336. const handleAssignOrder = useCallback(async (pickOrderId: number) => {
  337. if (!currentUserId) {
  338. console.error("Missing user id in session");
  339. return;
  340. }
  341. try {
  342. const result = await assignJobOrderPickOrder(pickOrderId, currentUserId);
  343. if (result.message === "Successfully assigned") {
  344. console.log(" Successfully assigned pick order");
  345. // 刷新数据
  346. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  347. // 重新加载未分配订单列表
  348. loadUnassignedOrders();
  349. } else {
  350. console.warn("⚠️ Assignment failed:", result.message);
  351. alert(`Assignment failed: ${result.message}`);
  352. }
  353. } catch (error) {
  354. console.error("❌ Error assigning order:", error);
  355. alert("Error occurred during assignment");
  356. }
  357. }, [currentUserId, loadUnassignedOrders]);
  358. // Handle QR code button click
  359. const handleQrCodeClick = (pickOrderId: number) => {
  360. console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
  361. // TODO: Implement QR code functionality
  362. };
  363. // 修改:使用 Job Order API 获取数据
  364. const fetchJobOrderData = useCallback(async (userId?: number) => {
  365. setCombinedDataLoading(true);
  366. try {
  367. const userIdToUse = userId || currentUserId;
  368. console.log(" fetchJobOrderData called with userId:", userIdToUse);
  369. if (!userIdToUse) {
  370. console.warn("⚠️ No userId available, skipping API call");
  371. setJobOrderData(null);
  372. setCombinedLotData([]);
  373. setOriginalCombinedData([]);
  374. return;
  375. }
  376. window.dispatchEvent(new CustomEvent('jobOrderDataStatus', {
  377. detail: {
  378. hasData: false,
  379. tabIndex: 1
  380. }
  381. }));
  382. // 使用 Job Order API
  383. const jobOrderData = await fetchCompletedJobOrderPickOrders(userIdToUse);
  384. console.log(" Job Order data:", jobOrderData);
  385. console.log(" Pick Order Code from API:", jobOrderData.pickOrder?.code);
  386. console.log(" Expected Pick Order Code: P-20251003-001");
  387. setJobOrderData(jobOrderData);
  388. // Transform hierarchical data to flat structure for the table
  389. const flatLotData: any[] = [];
  390. if (jobOrderData.pickOrder && jobOrderData.pickOrderLines) {
  391. jobOrderData.pickOrderLines.forEach((line: any) => {
  392. if (line.lots && line.lots.length > 0) {
  393. line.lots.forEach((lot: any) => {
  394. flatLotData.push({
  395. // Pick order info
  396. pickOrderId: jobOrderData.pickOrder.id,
  397. pickOrderCode: jobOrderData.pickOrder.code,
  398. pickOrderConsoCode: jobOrderData.pickOrder.consoCode,
  399. pickOrderTargetDate: jobOrderData.pickOrder.targetDate,
  400. pickOrderType: jobOrderData.pickOrder.type,
  401. pickOrderStatus: jobOrderData.pickOrder.status,
  402. pickOrderAssignTo: jobOrderData.pickOrder.assignTo,
  403. // Pick order line info
  404. pickOrderLineId: line.id,
  405. pickOrderLineRequiredQty: line.requiredQty,
  406. pickOrderLineStatus: line.status,
  407. // Item info
  408. itemId: line.itemId,
  409. itemCode: line.itemCode,
  410. itemName: line.itemName,
  411. uomCode: line.uomCode,
  412. uomDesc: line.uomDesc,
  413. // Lot info
  414. lotId: lot.lotId,
  415. lotNo: lot.lotNo,
  416. expiryDate: lot.expiryDate,
  417. location: lot.location,
  418. availableQty: lot.availableQty,
  419. requiredQty: lot.requiredQty,
  420. actualPickQty: lot.actualPickQty,
  421. lotStatus: lot.lotStatus,
  422. lotAvailability: lot.lotAvailability,
  423. processingStatus: lot.processingStatus,
  424. stockOutLineId: lot.stockOutLineId,
  425. stockOutLineStatus: lot.stockOutLineStatus,
  426. stockOutLineQty: lot.stockOutLineQty,
  427. // Router info
  428. routerIndex: lot.routerIndex,
  429. matchStatus: lot.matchStatus,
  430. routerArea: lot.routerArea,
  431. routerRoute: lot.routerRoute,
  432. uomShortDesc: lot.uomShortDesc
  433. });
  434. });
  435. }
  436. });
  437. }
  438. console.log(" Transformed flat lot data:", flatLotData);
  439. setCombinedLotData(flatLotData);
  440. setOriginalCombinedData(flatLotData);
  441. const hasData = flatLotData.length > 0;
  442. window.dispatchEvent(new CustomEvent('jobOrderDataStatus', {
  443. detail: {
  444. hasData: hasData,
  445. tabIndex: 1
  446. }
  447. }));
  448. // 计算完成状态并发送事件
  449. const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) =>
  450. lot.processingStatus === 'completed'
  451. );
  452. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  453. detail: {
  454. allLotsCompleted: allCompleted,
  455. tabIndex: 1
  456. }
  457. }));
  458. // 发送完成状态事件,包含标签页信息
  459. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  460. detail: {
  461. allLotsCompleted: allCompleted,
  462. tabIndex: 0 // 明确指定这是来自标签页 0 的事件
  463. }
  464. }));
  465. } catch (error) {
  466. console.error("❌ Error fetching job order data:", error);
  467. setJobOrderData(null);
  468. setCombinedLotData([]);
  469. setOriginalCombinedData([]);
  470. // 如果加载失败,禁用打印按钮
  471. window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
  472. detail: {
  473. allLotsCompleted: false,
  474. tabIndex: 0
  475. }
  476. }));
  477. } finally {
  478. setCombinedDataLoading(false);
  479. }
  480. }, [currentUserId]);
  481. const handleSubmitAllScanned = useCallback(async () => {
  482. const scannedLots = combinedLotData.filter(lot =>
  483. lot.matchStatus === 'scanned' // Only submit items that are scanned but not yet submitted
  484. );
  485. if (scannedLots.length === 0) {
  486. console.log("No scanned items to submit");
  487. return;
  488. }
  489. setIsSubmittingAll(true);
  490. console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`);
  491. try {
  492. // Submit all items in parallel using Promise.all
  493. const submitPromises = scannedLots.map(async (lot) => {
  494. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
  495. console.log(`Submitting item ${lot.itemCode}: qty=${submitQty}`);
  496. const result = await submitSecondScanQuantity(
  497. lot.pickOrderId,
  498. lot.itemId,
  499. {
  500. qty: submitQty,
  501. isMissing: false,
  502. isBad: false,
  503. reason: undefined
  504. }
  505. );
  506. return { success: result.code === "SUCCESS", itemCode: lot.itemCode };
  507. });
  508. // Wait for all submissions to complete
  509. const results = await Promise.all(submitPromises);
  510. const successCount = results.filter(r => r.success).length;
  511. console.log(` Batch submit completed: ${successCount}/${scannedLots.length} items submitted`);
  512. // Refresh data once after all submissions
  513. await fetchJobOrderData();
  514. if (successCount > 0) {
  515. setQrScanSuccess(true);
  516. setTimeout(() => setQrScanSuccess(false), 2000);
  517. }
  518. } catch (error) {
  519. console.error("Error submitting all scanned items:", error);
  520. setQrScanError(true);
  521. } finally {
  522. setIsSubmittingAll(false);
  523. }
  524. }, [combinedLotData, fetchJobOrderData]);
  525. // Calculate scanned items count
  526. const scannedItemsCount = useMemo(() => {
  527. return combinedLotData.filter(lot => lot.matchStatus === 'scanned').length;
  528. }, [combinedLotData]);
  529. // 修改:初始化时加载数据
  530. useEffect(() => {
  531. if (session && currentUserId && !initializationRef.current) {
  532. console.log(" Session loaded, initializing job order...");
  533. initializationRef.current = true;
  534. // 加载 Job Order 数据
  535. fetchJobOrderData();
  536. // 加载未分配订单
  537. loadUnassignedOrders();
  538. }
  539. }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders]);
  540. // Add event listener for manual assignment
  541. useEffect(() => {
  542. const handlePickOrderAssigned = () => {
  543. console.log("🔄 Pick order assigned event received, refreshing data...");
  544. fetchJobOrderData();
  545. };
  546. window.addEventListener('pickOrderAssigned', handlePickOrderAssigned);
  547. return () => {
  548. window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned);
  549. };
  550. }, [fetchJobOrderData]);
  551. // Handle QR code submission for matched lot (external scanning)
  552. const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
  553. console.log(` Processing Second QR Code for lot: ${lotNo}`);
  554. // Check if this lot was already processed recently
  555. const lotKey = `${lotNo}_${Date.now()}`;
  556. if (processedQrCodes.has(lotNo)) {
  557. console.log(`⏭️ Lot ${lotNo} already processed, skipping...`);
  558. return;
  559. }
  560. const currentLotData = combinedLotData;
  561. const matchingLots = currentLotData.filter(lot =>
  562. lot.lotNo === lotNo ||
  563. lot.lotNo?.toLowerCase() === lotNo.toLowerCase()
  564. );
  565. if (matchingLots.length === 0) {
  566. console.error(`❌ Lot not found: ${lotNo}`);
  567. setQrScanError(true);
  568. setQrScanSuccess(false);
  569. return;
  570. }
  571. try {
  572. let successCount = 0;
  573. for (const matchingLot of matchingLots) {
  574. // Check if this specific item was already processed
  575. const itemKey = `${matchingLot.pickOrderId}_${matchingLot.itemId}`;
  576. if (processedQrCodes.has(itemKey)) {
  577. console.log(`⏭️ Item ${matchingLot.itemId} already processed, skipping...`);
  578. continue;
  579. }
  580. // Use the new second scan API
  581. const result = await updateSecondQrScanStatus(
  582. matchingLot.pickOrderId,
  583. matchingLot.itemId,
  584. currentUserId || 0,
  585. matchingLot.requiredQty || 1 // 传递实际的 required quantity
  586. );
  587. if (result.code === "SUCCESS") {
  588. successCount++;
  589. // Mark this item as processed
  590. setProcessedQrCodes(prev => new Set(prev).add(itemKey));
  591. console.log(` Second QR scan status updated for item ${matchingLot.itemId}`);
  592. } else {
  593. console.error(`❌ Failed to update second QR scan status: ${result.message}`);
  594. }
  595. }
  596. if (successCount > 0) {
  597. setQrScanSuccess(true);
  598. setQrScanError(false);
  599. // Set refreshing flag briefly to prevent duplicate processing
  600. setIsRefreshingData(true);
  601. await fetchJobOrderData(); // Refresh data
  602. // Clear refresh flag and success message after a short delay
  603. setTimeout(() => {
  604. setQrScanSuccess(false);
  605. setIsRefreshingData(false);
  606. }, 500);
  607. } else {
  608. setQrScanError(true);
  609. setQrScanSuccess(false);
  610. }
  611. } catch (error) {
  612. console.error("❌ Error processing second QR code:", error);
  613. setQrScanError(true);
  614. setQrScanSuccess(false);
  615. }
  616. }, [combinedLotData, fetchJobOrderData, processedQrCodes]);
  617. useEffect(() => {
  618. // Add isManualScanning and isRefreshingData checks
  619. if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) {
  620. return;
  621. }
  622. const latestQr = qrValues[qrValues.length - 1];
  623. // Check if this QR was already processed recently
  624. if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) {
  625. console.log("⏭️ QR code already processed, skipping...");
  626. return;
  627. }
  628. // Mark as processed
  629. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  630. setLastProcessedQr(latestQr);
  631. // Extract lot number from QR code
  632. let lotNo = '';
  633. try {
  634. const qrData = JSON.parse(latestQr);
  635. if (qrData.stockInLineId && qrData.itemId) {
  636. // For JSON QR codes, we need to fetch the lot number
  637. fetchStockInLineInfo(qrData.stockInLineId)
  638. .then((stockInLineInfo) => {
  639. console.log("Outside QR scan - Stock in line info:", stockInLineInfo);
  640. const extractedLotNo = stockInLineInfo.lotNo;
  641. if (extractedLotNo) {
  642. console.log(`Outside QR scan detected (JSON): ${extractedLotNo}`);
  643. handleQrCodeSubmit(extractedLotNo);
  644. }
  645. })
  646. .catch((error) => {
  647. console.error("Outside QR scan - Error fetching stock in line info:", error);
  648. });
  649. return; // Exit early for JSON QR codes
  650. }
  651. } catch (error) {
  652. // Not JSON format, treat as direct lot number
  653. lotNo = latestQr.replace(/[{}]/g, '');
  654. }
  655. // For direct lot number QR codes
  656. if (lotNo) {
  657. console.log(`Outside QR scan detected (direct): ${lotNo}`);
  658. handleQrCodeSubmit(lotNo);
  659. }
  660. }, [qrValues, combinedLotData, handleQrCodeSubmit, processedQrCodes, lastProcessedQr, isManualScanning, isRefreshingData]);
  661. // ADD THIS: Cleanup effect
  662. useEffect(() => {
  663. return () => {
  664. // Cleanup when component unmounts (e.g., when switching tabs)
  665. if (isManualScanning) {
  666. console.log("🧹 Second scan component unmounting, stopping QR scanner...");
  667. stopScan();
  668. resetScan();
  669. }
  670. };
  671. }, [isManualScanning, stopScan, resetScan]);
  672. const handleManualInputSubmit = useCallback(() => {
  673. if (qrScanInput.trim() !== '') {
  674. handleQrCodeSubmit(qrScanInput.trim());
  675. }
  676. }, [qrScanInput, handleQrCodeSubmit]);
  677. // Handle QR code submission from modal (internal scanning)
  678. const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
  679. if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
  680. console.log(` QR Code verified for lot: ${lotNo}`);
  681. const requiredQty = selectedLotForQr.requiredQty;
  682. const lotId = selectedLotForQr.lotId;
  683. // Create stock out line
  684. const stockOutLineData: CreateStockOutLine = {
  685. consoCode: selectedLotForQr.pickOrderConsoCode,
  686. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  687. inventoryLotLineId: selectedLotForQr.lotId,
  688. qty: 0.0
  689. };
  690. try {
  691. // Close modal
  692. setQrModalOpen(false);
  693. setSelectedLotForQr(null);
  694. // Set pick quantity
  695. const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`;
  696. setTimeout(() => {
  697. setPickQtyData(prev => ({
  698. ...prev,
  699. [lotKey]: requiredQty
  700. }));
  701. console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
  702. }, 500);
  703. // Refresh data
  704. await fetchJobOrderData();
  705. } catch (error) {
  706. console.error("Error creating stock out line:", error);
  707. }
  708. }
  709. }, [selectedLotForQr, fetchJobOrderData]);
  710. // Outside QR scanning - process QR codes from outside the page automatically
  711. const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
  712. if (value === '' || value === null || value === undefined) {
  713. setPickQtyData(prev => ({
  714. ...prev,
  715. [lotKey]: 0
  716. }));
  717. return;
  718. }
  719. const numericValue = typeof value === 'string' ? parseFloat(value) : value;
  720. if (isNaN(numericValue)) {
  721. setPickQtyData(prev => ({
  722. ...prev,
  723. [lotKey]: 0
  724. }));
  725. return;
  726. }
  727. setPickQtyData(prev => ({
  728. ...prev,
  729. [lotKey]: numericValue
  730. }));
  731. }, []);
  732. const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle');
  733. const [autoAssignMessage, setAutoAssignMessage] = useState<string>('');
  734. const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
  735. try {
  736. // Use the new second scan submit API
  737. const result = await submitSecondScanQuantity(
  738. lot.pickOrderId,
  739. lot.itemId,
  740. {
  741. qty: submitQty,
  742. isMissing: false,
  743. isBad: false,
  744. reason: undefined // Fix TypeScript error
  745. }
  746. );
  747. if (result.code === "SUCCESS") {
  748. console.log(` Second scan quantity submitted: ${submitQty}`);
  749. await fetchJobOrderData(); // Refresh data
  750. } else {
  751. console.error(`❌ Failed to submit second scan quantity: ${result.message}`);
  752. }
  753. } catch (error) {
  754. console.error("Error submitting second scan quantity:", error);
  755. }
  756. }, [fetchJobOrderData]);
  757. // Handle reject lot
  758. // Handle pick execution form
  759. const handlePickExecutionForm = useCallback((lot: any) => {
  760. console.log("=== Pick Execution Form ===");
  761. console.log("Lot data:", lot);
  762. console.log("lot.pickOrderCode:", lot.pickOrderCode); // 添加
  763. console.log("lot.pickOrderId:", lot.pickOrderId);
  764. if (!lot) {
  765. console.warn("No lot data provided for pick execution form");
  766. return;
  767. }
  768. console.log("Opening pick execution form for lot:", lot.lotNo);
  769. setSelectedLotForExecutionForm(lot);
  770. setPickExecutionFormOpen(true);
  771. console.log("Pick execution form opened for lot ID:", lot.lotId);
  772. }, []);
  773. const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
  774. try {
  775. console.log("Pick execution form submitted:", data);
  776. if (!currentUserId) {
  777. console.error("❌ No current user ID available");
  778. return;
  779. }
  780. const result = await recordSecondScanIssue(
  781. selectedLotForExecutionForm.pickOrderId,
  782. selectedLotForExecutionForm.itemId,
  783. {
  784. qty: data.actualPickQty, // verified qty
  785. missQty: data.missQty || 0, // 添加:实际的 miss qty
  786. badItemQty: data.badItemQty || 0, // 添加:实际的 bad item qty
  787. isMissing: data.missQty > 0,
  788. isBad: data.badItemQty > 0,
  789. reason: data.issueRemark || '',
  790. createdBy: currentUserId,
  791. type: "match"
  792. }
  793. );
  794. console.log("Pick execution issue recorded:", result);
  795. if (result && result.code === "SUCCESS") {
  796. console.log(" Pick execution issue recorded successfully");
  797. } else {
  798. console.error("❌ Failed to record pick execution issue:", result);
  799. }
  800. setPickExecutionFormOpen(false);
  801. setSelectedLotForExecutionForm(null);
  802. await fetchJobOrderData();
  803. } catch (error) {
  804. console.error("Error submitting pick execution form:", error);
  805. }
  806. }, [currentUserId, selectedLotForExecutionForm, fetchJobOrderData,]);
  807. // Calculate remaining required quantity
  808. const calculateRemainingRequiredQty = useCallback((lot: any) => {
  809. const requiredQty = lot.requiredQty || 0;
  810. const stockOutLineQty = lot.stockOutLineQty || 0;
  811. return Math.max(0, requiredQty - stockOutLineQty);
  812. }, []);
  813. // Search criteria
  814. const searchCriteria: Criterion<any>[] = [
  815. {
  816. label: t("Pick Order Code"),
  817. paramName: "pickOrderCode",
  818. type: "text",
  819. },
  820. {
  821. label: t("Item Code"),
  822. paramName: "itemCode",
  823. type: "text",
  824. },
  825. {
  826. label: t("Item Name"),
  827. paramName: "itemName",
  828. type: "text",
  829. },
  830. {
  831. label: t("Lot No"),
  832. paramName: "lotNo",
  833. type: "text",
  834. },
  835. ];
  836. const handleSearch = useCallback((query: Record<string, any>) => {
  837. setSearchQuery({ ...query });
  838. console.log("Search query:", query);
  839. if (!originalCombinedData) return;
  840. const filtered = originalCombinedData.filter((lot: any) => {
  841. const pickOrderCodeMatch = !query.pickOrderCode ||
  842. lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
  843. const itemCodeMatch = !query.itemCode ||
  844. lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
  845. const itemNameMatch = !query.itemName ||
  846. lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase());
  847. const lotNoMatch = !query.lotNo ||
  848. lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase());
  849. return pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch;
  850. });
  851. setCombinedLotData(filtered);
  852. console.log("Filtered lots count:", filtered.length);
  853. }, [originalCombinedData]);
  854. const handleReset = useCallback(() => {
  855. setSearchQuery({});
  856. if (originalCombinedData) {
  857. setCombinedLotData(originalCombinedData);
  858. }
  859. }, [originalCombinedData]);
  860. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  861. setPaginationController(prev => ({
  862. ...prev,
  863. pageNum: newPage,
  864. }));
  865. }, []);
  866. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  867. const newPageSize = parseInt(event.target.value, 10);
  868. setPaginationController({
  869. pageNum: 0,
  870. pageSize: newPageSize,
  871. });
  872. }, []);
  873. // Pagination data with sorting by routerIndex
  874. const paginatedData = useMemo(() => {
  875. // Sort by routerIndex first, then by other criteria
  876. const sortedData = [...combinedLotData].sort((a, b) => {
  877. const aIndex = a.routerIndex || 0;
  878. const bIndex = b.routerIndex || 0;
  879. // Primary sort: by routerIndex
  880. if (aIndex !== bIndex) {
  881. return aIndex - bIndex;
  882. }
  883. // Secondary sort: by pickOrderCode if routerIndex is the same
  884. if (a.pickOrderCode !== b.pickOrderCode) {
  885. return a.pickOrderCode.localeCompare(b.pickOrderCode);
  886. }
  887. // Tertiary sort: by lotNo if everything else is the same
  888. return (a.lotNo || '').localeCompare(b.lotNo || '');
  889. });
  890. const startIndex = paginationController.pageNum * paginationController.pageSize;
  891. const endIndex = startIndex + paginationController.pageSize;
  892. return sortedData.slice(startIndex, endIndex);
  893. }, [combinedLotData, paginationController]);
  894. // Add these functions for manual scanning
  895. const handleStartScan = useCallback(() => {
  896. console.log(" Starting manual QR scan...");
  897. setIsManualScanning(true);
  898. setProcessedQrCodes(new Set());
  899. setLastProcessedQr('');
  900. setQrScanError(false);
  901. setQrScanSuccess(false);
  902. startScan();
  903. }, [startScan]);
  904. const handleStopScan = useCallback(() => {
  905. console.log("⏹️ Stopping manual QR scan...");
  906. setIsManualScanning(false);
  907. setQrScanError(false);
  908. setQrScanSuccess(false);
  909. stopScan();
  910. resetScan();
  911. }, [stopScan, resetScan]);
  912. useEffect(() => {
  913. if (isManualScanning && combinedLotData.length === 0) {
  914. console.log("⏹️ No data available, auto-stopping QR scan...");
  915. handleStopScan();
  916. }
  917. }, [combinedLotData.length, isManualScanning, handleStopScan]);
  918. // Cleanup effect
  919. useEffect(() => {
  920. return () => {
  921. // Cleanup when component unmounts (e.g., when switching tabs)
  922. if (isManualScanning) {
  923. console.log("🧹 Second scan component unmounting, stopping QR scanner...");
  924. stopScan();
  925. resetScan();
  926. }
  927. };
  928. }, [isManualScanning, stopScan, resetScan]);
  929. const getStatusMessage = useCallback((lot: any) => {
  930. switch (lot.stockOutLineStatus?.toLowerCase()) {
  931. case 'pending':
  932. return t("Please finish QR code scan and pick order.");
  933. case 'checked':
  934. return t("Please submit the pick order.");
  935. case 'partially_completed':
  936. return t("Partial quantity submitted. Please submit more or complete the order.");
  937. case 'completed':
  938. return t("Pick order completed successfully!");
  939. case 'rejected':
  940. return t("Lot has been rejected and marked as unavailable.");
  941. case 'unavailable':
  942. return t("This order is insufficient, please pick another lot.");
  943. default:
  944. return t("Please finish QR code scan and pick order.");
  945. }
  946. }, [t]);
  947. return (
  948. <TestQrCodeProvider
  949. lotData={combinedLotData}
  950. onScanLot={handleQrCodeSubmit}
  951. filterActive={(lot) => (
  952. lot.matchStatus !== 'completed' &&
  953. lot.lotAvailability !== 'rejected'
  954. )}
  955. >
  956. <FormProvider {...formProps}>
  957. <Stack spacing={2}>
  958. {/* Job Order Header */}
  959. {jobOrderData && (
  960. <Paper sx={{ p: 2 }}>
  961. <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
  962. <Typography variant="subtitle1">
  963. <strong>{t("Job Order")}:</strong> {jobOrderData.pickOrder?.jobOrder?.code || '-'}
  964. </Typography>
  965. <Typography variant="subtitle1">
  966. <strong>{t("Pick Order Code")}:</strong> {jobOrderData.pickOrder?.code || '-'}
  967. </Typography>
  968. <Typography variant="subtitle1">
  969. <strong>{t("Target Date")}:</strong> {jobOrderData.pickOrder?.targetDate || '-'}
  970. </Typography>
  971. </Stack>
  972. </Paper>
  973. )}
  974. {/* Combined Lot Table */}
  975. <Box>
  976. <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
  977. <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
  978. {!isManualScanning ? (
  979. <Button
  980. variant="contained"
  981. startIcon={<QrCodeIcon />}
  982. onClick={handleStartScan}
  983. color="primary"
  984. sx={{ minWidth: '120px' }}
  985. >
  986. {t("Start QR Scan")}
  987. </Button>
  988. ) : (
  989. <Button
  990. variant="outlined"
  991. startIcon={<QrCodeIcon />}
  992. onClick={handleStopScan}
  993. color="secondary"
  994. sx={{ minWidth: '120px' }}
  995. >
  996. {t("Stop QR Scan")}
  997. </Button>
  998. )}
  999. {/* ADD THIS: Submit All Scanned Button */}
  1000. <Button
  1001. variant="contained"
  1002. color="success"
  1003. onClick={handleSubmitAllScanned}
  1004. disabled={scannedItemsCount === 0 || isSubmittingAll}
  1005. sx={{ minWidth: '160px' }}
  1006. >
  1007. {isSubmittingAll ? (
  1008. <>
  1009. <CircularProgress size={16} sx={{ mr: 1 }} />
  1010. {t("Submitting...")}
  1011. </>
  1012. ) : (
  1013. `${t("Submit All Scanned")} (${scannedItemsCount})`
  1014. )}
  1015. </Button>
  1016. </Box>
  1017. </Box>
  1018. {qrScanError && !qrScanSuccess && (
  1019. <Alert severity="error" sx={{ mb: 2 }}>
  1020. {t("QR code does not match any item in current orders.")}
  1021. </Alert>
  1022. )}
  1023. {qrScanSuccess && (
  1024. <Alert severity="success" sx={{ mb: 2 }}>
  1025. {t("QR code verified.")}
  1026. </Alert>
  1027. )}
  1028. <TableContainer component={Paper}>
  1029. <Table>
  1030. <TableHead>
  1031. <TableRow>
  1032. <TableCell>{t("Index")}</TableCell>
  1033. <TableCell>{t("Route")}</TableCell>
  1034. <TableCell>{t("Item Code")}</TableCell>
  1035. <TableCell>{t("Item Name")}</TableCell>
  1036. <TableCell>{t("Lot No")}</TableCell>
  1037. <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
  1038. <TableCell align="center">{t("Scan Result")}</TableCell>
  1039. <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
  1040. </TableRow>
  1041. </TableHead>
  1042. <TableBody>
  1043. {paginatedData.length === 0 ? (
  1044. <TableRow>
  1045. <TableCell colSpan={8} align="center">
  1046. <Typography variant="body2" color="text.secondary">
  1047. {t("No data available")}
  1048. </Typography>
  1049. </TableCell>
  1050. </TableRow>
  1051. ) : (
  1052. paginatedData.map((lot, index) => (
  1053. <TableRow
  1054. key={`${lot.pickOrderLineId}-${lot.lotId}`}
  1055. sx={{
  1056. backgroundColor: lot.lotAvailability === 'rejected' ? 'grey.100' : 'inherit',
  1057. opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1,
  1058. '& .MuiTableCell-root': {
  1059. color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit'
  1060. }
  1061. }}
  1062. >
  1063. <TableCell>
  1064. <Typography variant="body2" fontWeight="bold">
  1065. {index + 1}
  1066. </Typography>
  1067. </TableCell>
  1068. <TableCell>
  1069. <Typography variant="body2">
  1070. {lot.routerRoute || '-'}
  1071. </Typography>
  1072. </TableCell>
  1073. <TableCell>{lot.itemCode}</TableCell>
  1074. <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell>
  1075. <TableCell>
  1076. <Box>
  1077. <Typography
  1078. sx={{
  1079. color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit',
  1080. opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1
  1081. }}
  1082. >
  1083. {lot.lotNo}
  1084. </Typography>
  1085. </Box>
  1086. </TableCell>
  1087. <TableCell align="right">
  1088. {(() => {
  1089. const requiredQty = lot.requiredQty || 0;
  1090. return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')';
  1091. })()}
  1092. </TableCell>
  1093. <TableCell align="center">
  1094. {lot.matchStatus?.toLowerCase() === 'scanned' ||
  1095. lot.matchStatus?.toLowerCase() === 'completed' ? (
  1096. <Box sx={{
  1097. display: 'flex',
  1098. justifyContent: 'center',
  1099. alignItems: 'center',
  1100. width: '100%',
  1101. height: '100%'
  1102. }}>
  1103. <Checkbox
  1104. checked={true} // 改为 true
  1105. disabled={true}
  1106. readOnly={true}
  1107. size="large"
  1108. sx={{
  1109. color: 'success.main', // 固定为绿色
  1110. '&.Mui-checked': {
  1111. color: 'success.main',
  1112. },
  1113. transform: 'scale(1.3)',
  1114. '& .MuiSvgIcon-root': {
  1115. fontSize: '1.5rem',
  1116. }
  1117. }}
  1118. />
  1119. </Box>
  1120. ) : (
  1121. <Typography variant="body2" color="text.secondary">
  1122. {t(" ")}
  1123. </Typography>
  1124. )}
  1125. </TableCell>
  1126. <TableCell align="center">
  1127. <Box sx={{ display: 'flex', justifyContent: 'center' }}>
  1128. <Stack direction="row" spacing={1} alignItems="center">
  1129. <Button
  1130. variant="contained"
  1131. onClick={() => {
  1132. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  1133. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
  1134. // Submit with default lot required pick qty
  1135. handlePickQtyChange(lotKey, submitQty);
  1136. handleSubmitPickQtyWithQty(lot, submitQty);
  1137. }}
  1138. disabled={
  1139. // 修复:只有扫描过但未完成的才能提交
  1140. lot.matchStatus !== 'scanned' || // 只有 scanned 状态才能提交
  1141. lot.lotAvailability === 'expired' ||
  1142. lot.lotAvailability === 'status_unavailable' ||
  1143. lot.lotAvailability === 'rejected'
  1144. }
  1145. sx={{
  1146. fontSize: '0.75rem',
  1147. py: 0.5,
  1148. minHeight: '28px',
  1149. minWidth: '70px'
  1150. }}
  1151. >
  1152. {t("Submit")}
  1153. </Button>
  1154. <Button
  1155. variant="outlined"
  1156. size="small"
  1157. onClick={() => handlePickExecutionForm(lot)}
  1158. disabled={
  1159. // 修复:只有扫描过但未完成的才能报告问题
  1160. lot.matchStatus !== 'scanned' || // 只有 scanned 状态才能报告问题
  1161. lot.lotAvailability === 'expired' ||
  1162. lot.lotAvailability === 'status_unavailable' ||
  1163. lot.lotAvailability === 'rejected'
  1164. }
  1165. sx={{
  1166. fontSize: '0.7rem',
  1167. py: 0.5,
  1168. minHeight: '28px',
  1169. minWidth: '60px',
  1170. borderColor: 'warning.main',
  1171. color: 'warning.main'
  1172. }}
  1173. title="Report missing or bad items"
  1174. >
  1175. {t("Issue")}
  1176. </Button>
  1177. </Stack>
  1178. </Box>
  1179. </TableCell>
  1180. </TableRow>
  1181. ))
  1182. )}
  1183. </TableBody>
  1184. </Table>
  1185. </TableContainer>
  1186. <TablePagination
  1187. component="div"
  1188. count={combinedLotData.length}
  1189. page={paginationController.pageNum}
  1190. rowsPerPage={paginationController.pageSize}
  1191. onPageChange={handlePageChange}
  1192. onRowsPerPageChange={handlePageSizeChange}
  1193. rowsPerPageOptions={[10, 25, 50]}
  1194. labelRowsPerPage={t("Rows per page")}
  1195. labelDisplayedRows={({ from, to, count }) =>
  1196. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  1197. }
  1198. />
  1199. </Box>
  1200. </Stack>
  1201. {/* QR Code Modal */}
  1202. <QrCodeModal
  1203. open={qrModalOpen}
  1204. onClose={() => {
  1205. setQrModalOpen(false);
  1206. setSelectedLotForQr(null);
  1207. stopScan();
  1208. resetScan();
  1209. }}
  1210. lot={selectedLotForQr}
  1211. combinedLotData={combinedLotData}
  1212. onQrCodeSubmit={handleQrCodeSubmitFromModal}
  1213. />
  1214. {/* Pick Execution Form Modal */}
  1215. {pickExecutionFormOpen && selectedLotForExecutionForm && (
  1216. <GoodPickExecutionForm
  1217. open={pickExecutionFormOpen}
  1218. onClose={() => {
  1219. setPickExecutionFormOpen(false);
  1220. setSelectedLotForExecutionForm(null);
  1221. }}
  1222. onSubmit={handlePickExecutionFormSubmit}
  1223. selectedLot={selectedLotForExecutionForm}
  1224. selectedPickOrderLine={{
  1225. id: selectedLotForExecutionForm.pickOrderLineId,
  1226. itemId: selectedLotForExecutionForm.itemId,
  1227. itemCode: selectedLotForExecutionForm.itemCode,
  1228. itemName: selectedLotForExecutionForm.itemName,
  1229. pickOrderCode: selectedLotForExecutionForm.pickOrderCode,
  1230. // Add missing required properties from GetPickOrderLineInfo interface
  1231. availableQty: selectedLotForExecutionForm.availableQty || 0,
  1232. requiredQty: selectedLotForExecutionForm.requiredQty || 0,
  1233. uomCode: selectedLotForExecutionForm.uomCode || '',
  1234. uomDesc: selectedLotForExecutionForm.uomDesc || '',
  1235. pickedQty: selectedLotForExecutionForm.actualPickQty || 0, // Use pickedQty instead of actualPickQty
  1236. suggestedList: [] // Add required suggestedList property
  1237. }}
  1238. pickOrderId={selectedLotForExecutionForm.pickOrderId}
  1239. pickOrderCreateDate={new Date()}
  1240. />
  1241. )}
  1242. </FormProvider>
  1243. </TestQrCodeProvider>
  1244. );
  1245. };
  1246. export default JobPickExecution