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

newJobPickExecution.tsx 98 KiB

2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
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ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
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ヶ月前
1ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
1ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615
  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, startTransition } from "react";
  23. import { useTranslation } from "react-i18next";
  24. import { useRouter } from "next/navigation";
  25. import {
  26. updateStockOutLineStatus,
  27. createStockOutLine,
  28. recordPickExecutionIssue,
  29. fetchFGPickOrders,
  30. FGPickOrderResponse,
  31. autoAssignAndReleasePickOrder,
  32. AutoAssignReleaseResponse,
  33. checkPickOrderCompletion,
  34. PickOrderCompletionResponse,
  35. checkAndCompletePickOrderByConsoCode,
  36. confirmLotSubstitution,
  37. updateStockOutLineStatusByQRCodeAndLotNo, // ✅ 添加
  38. batchSubmitList, // ✅ 添加
  39. batchSubmitListRequest, // ✅ 添加
  40. batchSubmitListLineRequest,
  41. } from "@/app/api/pickOrder/actions";
  42. // 修改:使用 Job Order API
  43. import {
  44. assignJobOrderPickOrder,
  45. fetchJobOrderLotsHierarchicalByPickOrderId,
  46. updateJoPickOrderHandledBy,
  47. JobOrderLotsHierarchicalResponse,
  48. } from "@/app/api/jo/actions";
  49. import { fetchNameList, NameList } from "@/app/api/user/actions";
  50. import {
  51. FormProvider,
  52. useForm,
  53. } from "react-hook-form";
  54. import SearchBox, { Criterion } from "../SearchBox";
  55. import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
  56. import { updateInventoryLotLineQuantities, analyzeQrCode, fetchLotDetail } from "@/app/api/inventory/actions";
  57. import QrCodeIcon from '@mui/icons-material/QrCode';
  58. import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
  59. import { useSession } from "next-auth/react";
  60. import { SessionWithTokens } from "@/config/authConfig";
  61. import { fetchStockInLineInfo } from "@/app/api/po/actions";
  62. import GoodPickExecutionForm from "./JobPickExecutionForm";
  63. import FGPickOrderCard from "./FGPickOrderCard";
  64. import LotConfirmationModal from "./LotConfirmationModal";
  65. import LinearProgressWithLabel from "../common/LinearProgressWithLabel";
  66. import ScanStatusAlert from "../common/ScanStatusAlert";
  67. interface Props {
  68. filterArgs: Record<string, any>;
  69. //onSwitchToRecordTab: () => void;
  70. onBackToList?: () => void;
  71. }
  72. // Manual Lot Confirmation Modal (align with GoodPickExecutiondetail, opened by {2fic})
  73. const ManualLotConfirmationModal: React.FC<{
  74. open: boolean;
  75. onClose: () => void;
  76. onConfirm: (expectedLotNo: string, scannedLotNo: string) => void;
  77. expectedLot: { lotNo: string; itemCode: string; itemName: string } | null;
  78. scannedLot: { lotNo: string; itemCode: string; itemName: string } | null;
  79. isLoading?: boolean;
  80. }> = ({ open, onClose, onConfirm, expectedLot, scannedLot, isLoading = false }) => {
  81. const { t } = useTranslation("jo");
  82. const [expectedLotInput, setExpectedLotInput] = useState<string>('');
  83. const [scannedLotInput, setScannedLotInput] = useState<string>('');
  84. const [error, setError] = useState<string>('');
  85. useEffect(() => {
  86. if (open) {
  87. setExpectedLotInput(expectedLot?.lotNo || '');
  88. setScannedLotInput(scannedLot?.lotNo || '');
  89. setError('');
  90. }
  91. }, [open, expectedLot, scannedLot]);
  92. const handleConfirm = () => {
  93. if (!expectedLotInput.trim() || !scannedLotInput.trim()) {
  94. setError(t("Please enter both expected and scanned lot numbers."));
  95. return;
  96. }
  97. if (expectedLotInput.trim() === scannedLotInput.trim()) {
  98. setError(t("Expected and scanned lot numbers cannot be the same."));
  99. return;
  100. }
  101. onConfirm(expectedLotInput.trim(), scannedLotInput.trim());
  102. };
  103. return (
  104. <Modal open={open} onClose={onClose}>
  105. <Box sx={{
  106. position: 'absolute',
  107. top: '50%',
  108. left: '50%',
  109. transform: 'translate(-50%, -50%)',
  110. bgcolor: 'background.paper',
  111. p: 3,
  112. borderRadius: 2,
  113. minWidth: 500,
  114. }}>
  115. <Typography variant="h6" gutterBottom color="warning.main">
  116. {t("Manual Lot Confirmation")}
  117. </Typography>
  118. <Box sx={{ mb: 2 }}>
  119. <Typography variant="body2" gutterBottom>
  120. <strong>{t("Expected Lot Number")}:</strong>
  121. </Typography>
  122. <TextField
  123. fullWidth
  124. size="small"
  125. value={expectedLotInput}
  126. onChange={(e) => { setExpectedLotInput(e.target.value); setError(''); }}
  127. sx={{ mb: 2 }}
  128. error={!!error && !expectedLotInput.trim()}
  129. />
  130. </Box>
  131. <Box sx={{ mb: 2 }}>
  132. <Typography variant="body2" gutterBottom>
  133. <strong>{t("Scanned Lot Number")}:</strong>
  134. </Typography>
  135. <TextField
  136. fullWidth
  137. size="small"
  138. value={scannedLotInput}
  139. onChange={(e) => { setScannedLotInput(e.target.value); setError(''); }}
  140. sx={{ mb: 2 }}
  141. error={!!error && !scannedLotInput.trim()}
  142. />
  143. </Box>
  144. {error && (
  145. <Box sx={{ mb: 2, p: 1, backgroundColor: '#ffebee', borderRadius: 1 }}>
  146. <Typography variant="body2" color="error">
  147. {error}
  148. </Typography>
  149. </Box>
  150. )}
  151. <Box sx={{ mt: 2, display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
  152. <Button onClick={onClose} variant="outlined" disabled={isLoading}>
  153. {t("Cancel")}
  154. </Button>
  155. <Button
  156. onClick={handleConfirm}
  157. variant="contained"
  158. color="warning"
  159. disabled={isLoading || !expectedLotInput.trim() || !scannedLotInput.trim()}
  160. >
  161. {isLoading ? t("Processing...") : t("Confirm")}
  162. </Button>
  163. </Box>
  164. </Box>
  165. </Modal>
  166. );
  167. };
  168. // QR Code Modal Component (from GoodPickExecution)
  169. const QrCodeModal: React.FC<{
  170. open: boolean;
  171. onClose: () => void;
  172. lot: any | null;
  173. onQrCodeSubmit: (lotNo: string) => void;
  174. combinedLotData: any[];
  175. }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => {
  176. const { t } = useTranslation("jo");
  177. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  178. const [manualInput, setManualInput] = useState<string>('');
  179. const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false);
  180. const [manualInputError, setManualInputError] = useState<boolean>(false);
  181. const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false);
  182. const [qrScanFailed, setQrScanFailed] = useState<boolean>(false);
  183. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  184. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  185. const [scannedQrResult, setScannedQrResult] = useState<string>('');
  186. // Process scanned QR codes
  187. useEffect(() => {
  188. if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) {
  189. const latestQr = qrValues[qrValues.length - 1];
  190. if (processedQrCodes.has(latestQr)) {
  191. console.log("QR code already processed, skipping...");
  192. return;
  193. }
  194. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  195. try {
  196. const qrData = JSON.parse(latestQr);
  197. if (qrData.stockInLineId && qrData.itemId) {
  198. setIsProcessingQr(true);
  199. setQrScanFailed(false);
  200. fetchStockInLineInfo(qrData.stockInLineId)
  201. .then((stockInLineInfo) => {
  202. console.log("Stock in line info:", stockInLineInfo);
  203. setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
  204. if (stockInLineInfo.lotNo === lot.lotNo) {
  205. console.log(` QR Code verified for lot: ${lot.lotNo}`);
  206. setQrScanSuccess(true);
  207. onQrCodeSubmit(lot.lotNo);
  208. onClose();
  209. resetScan();
  210. } else {
  211. console.log(`❌ QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`);
  212. setQrScanFailed(true);
  213. setManualInputError(true);
  214. setManualInputSubmitted(true);
  215. }
  216. })
  217. .catch((error) => {
  218. console.error("Error fetching stock in line info:", error);
  219. setScannedQrResult('Error fetching data');
  220. setQrScanFailed(true);
  221. setManualInputError(true);
  222. setManualInputSubmitted(true);
  223. })
  224. .finally(() => {
  225. setIsProcessingQr(false);
  226. });
  227. } else {
  228. const qrContent = latestQr.replace(/[{}]/g, '');
  229. setScannedQrResult(qrContent);
  230. if (qrContent === lot.lotNo) {
  231. setQrScanSuccess(true);
  232. onQrCodeSubmit(lot.lotNo);
  233. onClose();
  234. resetScan();
  235. } else {
  236. setQrScanFailed(true);
  237. setManualInputError(true);
  238. setManualInputSubmitted(true);
  239. }
  240. }
  241. } catch (error) {
  242. console.log("QR code is not JSON format, trying direct comparison");
  243. const qrContent = latestQr.replace(/[{}]/g, '');
  244. setScannedQrResult(qrContent);
  245. if (qrContent === lot.lotNo) {
  246. setQrScanSuccess(true);
  247. onQrCodeSubmit(lot.lotNo);
  248. onClose();
  249. resetScan();
  250. } else {
  251. setQrScanFailed(true);
  252. setManualInputError(true);
  253. setManualInputSubmitted(true);
  254. }
  255. }
  256. }
  257. }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes]);
  258. // Clear states when modal opens
  259. useEffect(() => {
  260. if (open) {
  261. setManualInput('');
  262. setManualInputSubmitted(false);
  263. setManualInputError(false);
  264. setIsProcessingQr(false);
  265. setQrScanFailed(false);
  266. setQrScanSuccess(false);
  267. setScannedQrResult('');
  268. setProcessedQrCodes(new Set());
  269. }
  270. }, [open]);
  271. useEffect(() => {
  272. if (lot) {
  273. setManualInput('');
  274. setManualInputSubmitted(false);
  275. setManualInputError(false);
  276. setIsProcessingQr(false);
  277. setQrScanFailed(false);
  278. setQrScanSuccess(false);
  279. setScannedQrResult('');
  280. setProcessedQrCodes(new Set());
  281. }
  282. }, [lot]);
  283. // Auto-submit manual input when it matches
  284. useEffect(() => {
  285. if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) {
  286. console.log(' Auto-submitting manual input:', manualInput.trim());
  287. const timer = setTimeout(() => {
  288. setQrScanSuccess(true);
  289. onQrCodeSubmit(lot.lotNo);
  290. onClose();
  291. setManualInput('');
  292. setManualInputError(false);
  293. setManualInputSubmitted(false);
  294. }, 200);
  295. return () => clearTimeout(timer);
  296. }
  297. }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]);
  298. const handleManualSubmit = () => {
  299. if (manualInput.trim() === lot?.lotNo) {
  300. setQrScanSuccess(true);
  301. onQrCodeSubmit(lot.lotNo);
  302. onClose();
  303. setManualInput('');
  304. } else {
  305. setQrScanFailed(true);
  306. setManualInputError(true);
  307. setManualInputSubmitted(true);
  308. }
  309. };
  310. useEffect(() => {
  311. if (open) {
  312. startScan();
  313. }
  314. }, [open, startScan]);
  315. return (
  316. <Modal open={open} onClose={onClose}>
  317. <Box sx={{
  318. position: 'absolute',
  319. top: '50%',
  320. left: '50%',
  321. transform: 'translate(-50%, -50%)',
  322. bgcolor: 'background.paper',
  323. p: 3,
  324. borderRadius: 2,
  325. minWidth: 400,
  326. }}>
  327. <Typography variant="h6" gutterBottom>
  328. {t("QR Code Scan for Lot")}: {lot?.lotNo}
  329. </Typography>
  330. {isProcessingQr && (
  331. <Box sx={{ mb: 2, p: 2, backgroundColor: '#e3f2fd', borderRadius: 1 }}>
  332. <Typography variant="body2" color="primary">
  333. {t("Processing QR code...")}
  334. </Typography>
  335. </Box>
  336. )}
  337. <Box sx={{ mb: 2 }}>
  338. <Typography variant="body2" gutterBottom>
  339. <strong>{t("Manual Input")}:</strong>
  340. </Typography>
  341. <TextField
  342. fullWidth
  343. size="small"
  344. value={manualInput}
  345. onChange={(e) => {
  346. setManualInput(e.target.value);
  347. if (qrScanFailed || manualInputError) {
  348. setQrScanFailed(false);
  349. setManualInputError(false);
  350. setManualInputSubmitted(false);
  351. }
  352. }}
  353. sx={{ mb: 1 }}
  354. error={manualInputSubmitted && manualInputError}
  355. helperText={
  356. manualInputSubmitted && manualInputError
  357. ? `${t("The input is not the same as the expected lot number.")}`
  358. : ''
  359. }
  360. />
  361. <Button
  362. variant="contained"
  363. onClick={handleManualSubmit}
  364. disabled={!manualInput.trim()}
  365. size="small"
  366. color="primary"
  367. >
  368. {t("Submit")}
  369. </Button>
  370. </Box>
  371. {qrValues.length > 0 && (
  372. <Box sx={{
  373. mb: 2,
  374. p: 2,
  375. backgroundColor: qrScanFailed ? '#ffebee' : qrScanSuccess ? '#e8f5e8' : '#f5f5f5',
  376. borderRadius: 1
  377. }}>
  378. <Typography variant="body2" color={qrScanFailed ? 'error' : qrScanSuccess ? 'success' : 'text.secondary'}>
  379. <strong>{t("QR Scan Result:")}</strong> {scannedQrResult}
  380. </Typography>
  381. {qrScanSuccess && (
  382. <Typography variant="caption" color="success" display="block">
  383. {t("Verified successfully!")}
  384. </Typography>
  385. )}
  386. </Box>
  387. )}
  388. <Box sx={{ mt: 2, textAlign: 'right' }}>
  389. <Button onClick={onClose} variant="outlined">
  390. {t("Cancel")}
  391. </Button>
  392. </Box>
  393. </Box>
  394. </Modal>
  395. );
  396. };
  397. const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
  398. const { t } = useTranslation("jo");
  399. const router = useRouter();
  400. const { data: session } = useSession() as { data: SessionWithTokens | null };
  401. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  402. // 修改:使用 Job Order 数据结构
  403. const [combinedDataLoading, setCombinedDataLoading] = useState(false);
  404. // 添加未分配订单状态
  405. const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]);
  406. const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
  407. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  408. const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false);
  409. const [expectedLotData, setExpectedLotData] = useState<any>(null);
  410. const [scannedLotData, setScannedLotData] = useState<any>(null);
  411. const [isConfirmingLot, setIsConfirmingLot] = useState(false);
  412. const [qrScanInput, setQrScanInput] = useState<string>('');
  413. const [qrScanError, setQrScanError] = useState<boolean>(false);
  414. const [qrScanErrorMsg, setQrScanErrorMsg] = useState<string>('');
  415. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  416. const [jobOrderData, setJobOrderData] = useState<JobOrderLotsHierarchicalResponse | null>(null);
  417. const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({});
  418. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  419. const [paginationController, setPaginationController] = useState({
  420. pageNum: 0,
  421. pageSize: 10,
  422. });
  423. const [usernameList, setUsernameList] = useState<NameList[]>([]);
  424. const initializationRef = useRef(false);
  425. const autoAssignRef = useRef(false);
  426. const formProps = useForm();
  427. const errors = formProps.formState.errors;
  428. const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false);
  429. // Add QR modal states
  430. const [qrModalOpen, setQrModalOpen] = useState(false);
  431. const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);
  432. // Add GoodPickExecutionForm states
  433. const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
  434. const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
  435. const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);
  436. const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
  437. // Add these missing state variables
  438. const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
  439. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  440. const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
  441. const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
  442. // Track processed QR codes by itemId+stockInLineId combination for better lot confirmation handling
  443. const [processedQrCombinations, setProcessedQrCombinations] = useState<Map<number, Set<number>>>(new Map());
  444. // Cache for fetchStockInLineInfo API calls to avoid redundant requests
  445. const stockInLineInfoCache = useRef<Map<number, { lotNo: string | null; timestamp: number }>>(new Map());
  446. const CACHE_TTL = 60000; // 60 seconds cache TTL
  447. const abortControllerRef = useRef<AbortController | null>(null);
  448. const qrProcessingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  449. // Use refs for processed QR tracking to avoid useEffect dependency issues and delays
  450. const processedQrCodesRef = useRef<Set<string>>(new Set());
  451. const lastProcessedQrRef = useRef<string>('');
  452. // Store callbacks in refs to avoid useEffect dependency issues
  453. const processOutsideQrCodeRef = useRef<((latestQr: string) => Promise<void>) | null>(null);
  454. const resetScanRef = useRef<(() => void) | null>(null);
  455. // Manual lot confirmation modal state (test shortcut {2fic})
  456. const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = useState(false);
  457. const getAllLotsFromHierarchical = useCallback((
  458. data: JobOrderLotsHierarchicalResponse | null
  459. ): any[] => {
  460. if (!data || !data.pickOrder || !data.pickOrderLines) {
  461. return [];
  462. }
  463. const allLots: any[] = [];
  464. data.pickOrderLines.forEach((line) => {
  465. if (line.lots && line.lots.length > 0) {
  466. line.lots.forEach((lot) => {
  467. allLots.push({
  468. ...lot,
  469. pickOrderLineId: line.id,
  470. itemId: line.itemId,
  471. itemCode: line.itemCode,
  472. itemName: line.itemName,
  473. uomCode: line.uomCode,
  474. uomDesc: line.uomDesc,
  475. pickOrderLineRequiredQty: line.requiredQty,
  476. pickOrderLineStatus: line.status,
  477. jobOrderId: data.pickOrder.jobOrder.id,
  478. jobOrderCode: data.pickOrder.jobOrder.code,
  479. // 添加 pickOrder 信息(如果需要)
  480. pickOrderId: data.pickOrder.id,
  481. pickOrderCode: data.pickOrder.code,
  482. pickOrderConsoCode: data.pickOrder.consoCode,
  483. pickOrderTargetDate: data.pickOrder.targetDate,
  484. pickOrderType: data.pickOrder.type,
  485. pickOrderStatus: data.pickOrder.status,
  486. pickOrderAssignTo: data.pickOrder.assignTo,
  487. handler: line.handler,
  488. });
  489. });
  490. }
  491. });
  492. return allLots;
  493. }, []);
  494. const combinedLotData = useMemo(() => {
  495. return getAllLotsFromHierarchical(jobOrderData);
  496. }, [jobOrderData, getAllLotsFromHierarchical]);
  497. const originalCombinedData = useMemo(() => {
  498. return getAllLotsFromHierarchical(jobOrderData);
  499. }, [jobOrderData, getAllLotsFromHierarchical]);
  500. // Enhanced lotDataIndexes with cached active lots for better performance (align with GoodPickExecutiondetail)
  501. const lotDataIndexes = useMemo(() => {
  502. const byItemId = new Map<number, any[]>();
  503. const byItemCode = new Map<string, any[]>();
  504. const byLotId = new Map<number, any>();
  505. const byLotNo = new Map<string, any[]>();
  506. const byStockInLineId = new Map<number, any[]>();
  507. const activeLotsByItemId = new Map<number, any[]>();
  508. const rejectedStatuses = new Set(['rejected']);
  509. for (let i = 0; i < combinedLotData.length; i++) {
  510. const lot = combinedLotData[i];
  511. const isActive =
  512. !rejectedStatuses.has(lot.lotAvailability) &&
  513. !rejectedStatuses.has(lot.stockOutLineStatus) &&
  514. !rejectedStatuses.has(lot.processingStatus) &&
  515. lot.stockOutLineStatus !== 'completed';
  516. if (lot.itemId) {
  517. if (!byItemId.has(lot.itemId)) {
  518. byItemId.set(lot.itemId, []);
  519. activeLotsByItemId.set(lot.itemId, []);
  520. }
  521. byItemId.get(lot.itemId)!.push(lot);
  522. if (isActive) activeLotsByItemId.get(lot.itemId)!.push(lot);
  523. }
  524. if (lot.itemCode) {
  525. if (!byItemCode.has(lot.itemCode)) byItemCode.set(lot.itemCode, []);
  526. byItemCode.get(lot.itemCode)!.push(lot);
  527. }
  528. if (lot.lotId) byLotId.set(lot.lotId, lot);
  529. if (lot.lotNo) {
  530. if (!byLotNo.has(lot.lotNo)) byLotNo.set(lot.lotNo, []);
  531. byLotNo.get(lot.lotNo)!.push(lot);
  532. }
  533. if (lot.stockInLineId) {
  534. if (!byStockInLineId.has(lot.stockInLineId)) byStockInLineId.set(lot.stockInLineId, []);
  535. byStockInLineId.get(lot.stockInLineId)!.push(lot);
  536. }
  537. }
  538. return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId, activeLotsByItemId };
  539. }, [combinedLotData]);
  540. // Cached version of fetchStockInLineInfo to avoid redundant API calls
  541. const fetchStockInLineInfoCached = useCallback(async (stockInLineId: number): Promise<{ lotNo: string | null }> => {
  542. const now = Date.now();
  543. const cached = stockInLineInfoCache.current.get(stockInLineId);
  544. if (cached && (now - cached.timestamp) < CACHE_TTL) {
  545. return { lotNo: cached.lotNo };
  546. }
  547. if (abortControllerRef.current) abortControllerRef.current.abort();
  548. const abortController = new AbortController();
  549. abortControllerRef.current = abortController;
  550. const stockInLineInfo = await fetchStockInLineInfo(stockInLineId);
  551. stockInLineInfoCache.current.set(stockInLineId, {
  552. lotNo: stockInLineInfo.lotNo || null,
  553. timestamp: now
  554. });
  555. if (stockInLineInfoCache.current.size > 100) {
  556. const firstKey = stockInLineInfoCache.current.keys().next().value;
  557. if (firstKey !== undefined) stockInLineInfoCache.current.delete(firstKey);
  558. }
  559. return { lotNo: stockInLineInfo.lotNo || null };
  560. }, []);
  561. // 修改:加载未分配的 Job Order 订单
  562. const loadUnassignedOrders = useCallback(async () => {
  563. setIsLoadingUnassigned(true);
  564. try {
  565. //const orders = await fetchUnassignedJobOrderPickOrders();
  566. //setUnassignedOrders(orders);
  567. } catch (error) {
  568. console.error("Error loading unassigned orders:", error);
  569. } finally {
  570. setIsLoadingUnassigned(false);
  571. }
  572. }, []);
  573. // 修改:分配订单给当前用户
  574. const handleAssignOrder = useCallback(async (pickOrderId: number) => {
  575. if (!currentUserId) {
  576. console.error("Missing user id in session");
  577. return;
  578. }
  579. try {
  580. const result = await assignJobOrderPickOrder(pickOrderId, currentUserId);
  581. if (result.message === "Successfully assigned") {
  582. console.log(" Successfully assigned pick order");
  583. // 刷新数据
  584. window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
  585. // 重新加载未分配订单列表
  586. loadUnassignedOrders();
  587. } else {
  588. console.warn("⚠️ Assignment failed:", result.message);
  589. alert(`Assignment failed: ${result.message}`);
  590. }
  591. } catch (error) {
  592. console.error("❌ Error assigning order:", error);
  593. alert("Error occurred during assignment");
  594. }
  595. }, [currentUserId, loadUnassignedOrders]);
  596. const fetchFgPickOrdersData = useCallback(async () => {
  597. if (!currentUserId) return;
  598. setFgPickOrdersLoading(true);
  599. try {
  600. // Get all pick order IDs from combinedLotData
  601. const pickOrderIds = Array.from(new Set(combinedLotData.map(lot => lot.pickOrderId)));
  602. if (pickOrderIds.length === 0) {
  603. setFgPickOrders([]);
  604. return;
  605. }
  606. // Fetch FG pick orders for each pick order ID
  607. const fgPickOrdersPromises = pickOrderIds.map(pickOrderId =>
  608. fetchFGPickOrders(pickOrderId)
  609. );
  610. const fgPickOrdersResults = await Promise.all(fgPickOrdersPromises);
  611. // Flatten the results (each fetchFGPickOrders returns an array)
  612. const allFgPickOrders = fgPickOrdersResults.flat();
  613. setFgPickOrders(allFgPickOrders);
  614. console.log(" Fetched FG pick orders:", allFgPickOrders);
  615. } catch (error) {
  616. console.error("❌ Error fetching FG pick orders:", error);
  617. setFgPickOrders([]);
  618. } finally {
  619. setFgPickOrdersLoading(false);
  620. }
  621. }, [currentUserId, combinedLotData]);
  622. useEffect(() => {
  623. if (combinedLotData.length > 0) {
  624. fetchFgPickOrdersData();
  625. }
  626. }, [combinedLotData, fetchFgPickOrdersData]);
  627. // Handle QR code button click
  628. const handleQrCodeClick = (pickOrderId: number) => {
  629. console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
  630. // TODO: Implement QR code functionality
  631. };
  632. // 修改:使用 Job Order API 获取数据
  633. const fetchJobOrderData = useCallback(async (pickOrderId?: number) => {
  634. setCombinedDataLoading(true);
  635. try {
  636. if (!pickOrderId) {
  637. console.warn("⚠️ No pickOrderId provided, skipping API call");
  638. setJobOrderData(null);
  639. return;
  640. }
  641. // 直接使用类型化的响应
  642. const jobOrderData = await fetchJobOrderLotsHierarchicalByPickOrderId(pickOrderId);
  643. console.log("✅ Job Order data (hierarchical):", jobOrderData);
  644. setJobOrderData(jobOrderData);
  645. // 使用辅助函数获取所有 lots(不再扁平化)
  646. const allLots = getAllLotsFromHierarchical(jobOrderData);
  647. // ... 其他逻辑保持不变 ...
  648. } catch (error) {
  649. console.error("❌ Error fetching job order data:", error);
  650. setJobOrderData(null);
  651. } finally {
  652. setCombinedDataLoading(false);
  653. }
  654. }, [getAllLotsFromHierarchical]);
  655. const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => {
  656. if (!currentUserId || !pickOrderId || !itemId) {
  657. return;
  658. }
  659. try {
  660. console.log(`Updating JoPickOrder.handledBy for pickOrderId: ${pickOrderId}, itemId: ${itemId}, userId: ${currentUserId}`);
  661. await updateJoPickOrderHandledBy({
  662. pickOrderId: pickOrderId,
  663. itemId: itemId,
  664. userId: currentUserId
  665. });
  666. console.log("✅ JoPickOrder.handledBy updated successfully");
  667. } catch (error) {
  668. console.error("❌ Error updating JoPickOrder.handledBy:", error);
  669. // Don't throw - this is not critical for the main flow
  670. }
  671. }, [currentUserId]);
  672. // 修改:初始化时加载数据
  673. useEffect(() => {
  674. if (session && currentUserId && !initializationRef.current) {
  675. console.log("✅ Session loaded, initializing job order...");
  676. initializationRef.current = true;
  677. // Get pickOrderId from filterArgs if available (when viewing from list)
  678. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  679. if (pickOrderId) {
  680. fetchJobOrderData(pickOrderId);
  681. }
  682. loadUnassignedOrders();
  683. }
  684. }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders, filterArgs?.pickOrderId]);
  685. // Add event listener for manual assignment
  686. useEffect(() => {
  687. const handlePickOrderAssigned = () => {
  688. console.log("🔄 Pick order assigned event received, refreshing data...");
  689. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  690. if (pickOrderId) {
  691. fetchJobOrderData(pickOrderId);
  692. }
  693. };
  694. window.addEventListener('pickOrderAssigned', handlePickOrderAssigned);
  695. return () => {
  696. window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned);
  697. };
  698. }, [fetchJobOrderData, filterArgs?.pickOrderId]);
  699. // Handle QR code submission for matched lot (external scanning)
  700. const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
  701. console.log(` Processing QR Code for lot: ${lotNo}`);
  702. // Use current data without refreshing to avoid infinite loop
  703. const currentLotData = combinedLotData;
  704. console.log(`🔍 Available lots:`, currentLotData.map(lot => lot.lotNo));
  705. const matchingLots = currentLotData.filter(lot =>
  706. lot.lotNo === lotNo ||
  707. lot.lotNo?.toLowerCase() === lotNo.toLowerCase()
  708. );
  709. if (matchingLots.length === 0) {
  710. console.error(`❌ Lot not found: ${lotNo}`);
  711. setQrScanError(true);
  712. setQrScanSuccess(false);
  713. const availableLotNos = currentLotData.map(lot => lot.lotNo).join(', ');
  714. console.log(`❌ QR Code "${lotNo}" does not match any expected lots. Available lots: ${availableLotNos}`);
  715. return;
  716. }
  717. console.log(` Found ${matchingLots.length} matching lots:`, matchingLots);
  718. setQrScanError(false);
  719. try {
  720. let successCount = 0;
  721. let errorCount = 0;
  722. for (const matchingLot of matchingLots) {
  723. console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`);
  724. if (matchingLot.stockOutLineId) {
  725. const stockOutLineUpdate = await updateStockOutLineStatus({
  726. id: matchingLot.stockOutLineId,
  727. status: 'checked',
  728. qty: 0
  729. });
  730. console.log(`Update stock out line result for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate);
  731. // Treat multiple backend shapes as success (type-safe via any)
  732. const r: any = stockOutLineUpdate as any;
  733. const updateOk =
  734. r?.code === 'SUCCESS' ||
  735. typeof r?.id === 'number' ||
  736. r?.type === 'checked' ||
  737. r?.status === 'checked' ||
  738. typeof r?.entity?.id === 'number' ||
  739. r?.entity?.status === 'checked';
  740. if (updateOk) {
  741. successCount++;
  742. } else {
  743. errorCount++;
  744. }
  745. } else {
  746. const createStockOutLineData = {
  747. consoCode: matchingLot.pickOrderConsoCode,
  748. pickOrderLineId: matchingLot.pickOrderLineId,
  749. inventoryLotLineId: matchingLot.lotId,
  750. qty: 0
  751. };
  752. const createResult = await createStockOutLine(createStockOutLineData);
  753. console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, createResult);
  754. if (createResult && createResult.code === "SUCCESS") {
  755. // Immediately set status to checked for new line
  756. let newSolId: number | undefined;
  757. const anyRes: any = createResult as any;
  758. if (typeof anyRes?.id === 'number') {
  759. newSolId = anyRes.id;
  760. } else if (anyRes?.entity) {
  761. newSolId = Array.isArray(anyRes.entity) ? anyRes.entity[0]?.id : anyRes.entity?.id;
  762. }
  763. if (newSolId) {
  764. const setChecked = await updateStockOutLineStatus({
  765. id: newSolId,
  766. status: 'checked',
  767. qty: 0
  768. });
  769. if (setChecked && setChecked.code === "SUCCESS") {
  770. successCount++;
  771. } else {
  772. errorCount++;
  773. }
  774. } else {
  775. console.warn("Created stock out line but no ID returned; cannot set to checked");
  776. errorCount++;
  777. }
  778. } else {
  779. errorCount++;
  780. }
  781. }
  782. }
  783. // FIXED: Set refresh flag before refreshing data
  784. setIsRefreshingData(true);
  785. console.log("🔄 Refreshing data after QR code processing...");
  786. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  787. await fetchJobOrderData(pickOrderId);
  788. if (successCount > 0) {
  789. console.log(` QR Code processing completed: ${successCount} updated/created`);
  790. setQrScanSuccess(true);
  791. setQrScanError(false);
  792. setQrScanInput(''); // Clear input after successful processing
  793. } else {
  794. console.error(`❌ QR Code processing failed: ${errorCount} errors`);
  795. setQrScanError(true);
  796. setQrScanSuccess(false);
  797. }
  798. } catch (error) {
  799. console.error("❌ Error processing QR code:", error);
  800. setQrScanError(true);
  801. setQrScanSuccess(false);
  802. // Still refresh data even on error
  803. setIsRefreshingData(true);
  804. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  805. await fetchJobOrderData( pickOrderId);
  806. } finally {
  807. // Clear refresh flag after a short delay
  808. setTimeout(() => {
  809. setIsRefreshingData(false);
  810. }, 1000);
  811. }
  812. }, [combinedLotData, fetchJobOrderData]);
  813. const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => {
  814. console.log("⚠️ [LOT MISMATCH] Lot mismatch detected:", { expectedLot, scannedLot });
  815. console.log("⚠️ [LOT MISMATCH] Opening confirmation modal - NO lot will be marked as scanned until user confirms");
  816. // ✅ schedule modal open in next tick (avoid flushSync warnings on some builds)
  817. // ✅ IMPORTANT: This function ONLY opens the modal. It does NOT process any lot.
  818. setTimeout(() => {
  819. setExpectedLotData(expectedLot);
  820. setScannedLotData({
  821. ...scannedLot,
  822. lotNo: scannedLot.lotNo || null,
  823. });
  824. setLotConfirmationOpen(true);
  825. console.log("⚠️ [LOT MISMATCH] Modal opened - waiting for user confirmation");
  826. }, 0);
  827. // ✅ Fetch lotNo in background for display purposes (cached)
  828. // ✅ This is ONLY for display - it does NOT process any lot
  829. if (!scannedLot.lotNo && scannedLot.stockInLineId) {
  830. console.log(`⚠️ [LOT MISMATCH] Fetching lotNo for display (stockInLineId: ${scannedLot.stockInLineId})`);
  831. fetchStockInLineInfoCached(scannedLot.stockInLineId)
  832. .then((info) => {
  833. console.log(`⚠️ [LOT MISMATCH] Fetched lotNo for display: ${info.lotNo}`);
  834. startTransition(() => {
  835. setScannedLotData((prev: any) => ({
  836. ...prev,
  837. lotNo: info.lotNo || null,
  838. }));
  839. });
  840. })
  841. .catch((error) => {
  842. console.error(`❌ [LOT MISMATCH] Error fetching lotNo for display (stockInLineId may not exist):`, error);
  843. // ignore display fetch errors - this does NOT affect processing
  844. });
  845. }
  846. }, [fetchStockInLineInfoCached]);
  847. // Add handleLotConfirmation function
  848. const handleLotConfirmation = useCallback(async () => {
  849. if (!expectedLotData || !scannedLotData || !selectedLotForQr) {
  850. console.error("❌ [LOT CONFIRM] Missing required data for lot confirmation");
  851. return;
  852. }
  853. console.log("✅ [LOT CONFIRM] User confirmed lot substitution - processing now");
  854. console.log("✅ [LOT CONFIRM] Expected lot:", expectedLotData);
  855. console.log("✅ [LOT CONFIRM] Scanned lot:", scannedLotData);
  856. console.log("✅ [LOT CONFIRM] Selected lot for QR:", selectedLotForQr);
  857. setIsConfirmingLot(true);
  858. try {
  859. let newLotLineId = scannedLotData?.inventoryLotLineId;
  860. if (!newLotLineId && scannedLotData?.stockInLineId) {
  861. try {
  862. if (currentUserId && selectedLotForQr.pickOrderId && selectedLotForQr.itemId) {
  863. try {
  864. await updateHandledBy(selectedLotForQr.pickOrderId, selectedLotForQr.itemId);
  865. console.log(`✅ [LOT CONFIRM] Handler updated for itemId ${selectedLotForQr.itemId}`);
  866. } catch (error) {
  867. console.error(`❌ [LOT CONFIRM] Error updating handler (non-critical):`, error);
  868. }
  869. }
  870. console.log(`🔍 [LOT CONFIRM] Fetching lot detail for stockInLineId: ${scannedLotData.stockInLineId}`);
  871. const ld = await fetchLotDetail(scannedLotData.stockInLineId);
  872. newLotLineId = ld.inventoryLotLineId;
  873. console.log(`✅ [LOT CONFIRM] Fetched lot detail: inventoryLotLineId=${newLotLineId}`);
  874. } catch (error) {
  875. console.error("❌ [LOT CONFIRM] Error fetching lot detail (stockInLineId may not exist):", error);
  876. // If stockInLineId doesn't exist, we can still proceed with lotNo substitution
  877. // The backend confirmLotSubstitution should handle this case
  878. }
  879. }
  880. if (!newLotLineId) {
  881. console.warn("⚠️ [LOT CONFIRM] No inventory lot line id for scanned lot, proceeding with lotNo only");
  882. // Continue anyway - backend may handle lotNo substitution without inventoryLotLineId
  883. }
  884. console.log("=== [LOT CONFIRM] Lot Confirmation Debug ===");
  885. console.log("Selected Lot:", selectedLotForQr);
  886. console.log("Pick Order Line ID:", selectedLotForQr.pickOrderLineId);
  887. console.log("Stock Out Line ID:", selectedLotForQr.stockOutLineId);
  888. console.log("Suggested Pick Lot ID:", selectedLotForQr.suggestedPickLotId);
  889. console.log("Lot ID (fallback):", selectedLotForQr.lotId);
  890. console.log("New Inventory Lot Line ID:", newLotLineId);
  891. console.log("Scanned Lot No:", scannedLotData.lotNo);
  892. console.log("Scanned StockInLineId:", scannedLotData.stockInLineId);
  893. // Call confirmLotSubstitution to update the suggested lot
  894. console.log("🔄 [LOT CONFIRM] Calling confirmLotSubstitution...");
  895. const substitutionResult = await confirmLotSubstitution({
  896. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  897. stockOutLineId: selectedLotForQr.stockOutLineId,
  898. originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId,
  899. newInventoryLotNo: scannedLotData.lotNo || '',
  900. // ✅ required by LotSubstitutionConfirmRequest
  901. newStockInLineId: scannedLotData?.stockInLineId ?? null,
  902. });
  903. console.log("✅ [LOT CONFIRM] Lot substitution result:", substitutionResult);
  904. // ✅ CRITICAL: substitution failed => DO NOT mark original stockOutLine as checked.
  905. // Keep modal open so user can cancel/rescan.
  906. if (!substitutionResult || substitutionResult.code !== "SUCCESS") {
  907. console.error("❌ [LOT CONFIRM] Lot substitution failed. Will NOT update stockOutLine status.");
  908. setQrScanError(true);
  909. setQrScanSuccess(false);
  910. setQrScanErrorMsg(
  911. substitutionResult?.message ||
  912. `换批失败:stockInLineId ${scannedLotData?.stockInLineId ?? ""} 不存在或无法匹配`
  913. );
  914. return;
  915. }
  916. // Update stock out line status to 'checked' after substitution
  917. if(selectedLotForQr?.stockOutLineId){
  918. console.log(`🔄 [LOT CONFIRM] Updating stockOutLine ${selectedLotForQr.stockOutLineId} to 'checked'`);
  919. await updateStockOutLineStatus({
  920. id: selectedLotForQr.stockOutLineId,
  921. status: 'checked',
  922. qty: 0
  923. });
  924. console.log(`✅ [LOT CONFIRM] Stock out line ${selectedLotForQr.stockOutLineId} status updated to 'checked'`);
  925. }
  926. // Close modal and clean up state BEFORE refreshing
  927. setLotConfirmationOpen(false);
  928. setExpectedLotData(null);
  929. setScannedLotData(null);
  930. setSelectedLotForQr(null);
  931. // Clear QR processing state but DON'T clear processedQrCodes yet
  932. setQrScanError(false);
  933. setQrScanSuccess(true);
  934. setQrScanErrorMsg('');
  935. setQrScanInput('');
  936. // Set refreshing flag to prevent QR processing during refresh
  937. setIsRefreshingData(true);
  938. // Refresh data to show updated lot
  939. console.log("🔄 Refreshing job order data...");
  940. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  941. await fetchJobOrderData(pickOrderId);
  942. console.log(" Lot substitution confirmed and data refreshed");
  943. // Clear processed QR codes and flags immediately after refresh
  944. // This allows new QR codes to be processed right away
  945. setTimeout(() => {
  946. console.log(" Clearing processed QR codes and resuming scan");
  947. setProcessedQrCodes(new Set());
  948. setLastProcessedQr('');
  949. setQrScanSuccess(false);
  950. setIsRefreshingData(false);
  951. // ✅ Clear processedQrCombinations to allow reprocessing the same QR if needed
  952. if (scannedLotData?.stockInLineId && selectedLotForQr?.itemId) {
  953. setProcessedQrCombinations(prev => {
  954. const newMap = new Map(prev);
  955. const itemId = selectedLotForQr.itemId;
  956. if (itemId && newMap.has(itemId)) {
  957. newMap.get(itemId)!.delete(scannedLotData.stockInLineId);
  958. if (newMap.get(itemId)!.size === 0) {
  959. newMap.delete(itemId);
  960. }
  961. }
  962. return newMap;
  963. });
  964. }
  965. }, 500); // Reduced from 3000ms to 500ms - just enough for UI update
  966. } catch (error) {
  967. console.error("Error confirming lot substitution:", error);
  968. setQrScanError(true);
  969. setQrScanSuccess(false);
  970. setQrScanErrorMsg('换批发生异常,请重试或联系管理员');
  971. // Clear refresh flag on error
  972. setIsRefreshingData(false);
  973. } finally {
  974. setIsConfirmingLot(false);
  975. }
  976. }, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData,currentUserId, updateHandledBy ]);
  977. const processOutsideQrCode = useCallback(async (latestQr: string) => {
  978. // ✅ Only JSON QR supported for outside scanner (avoid false positive with lotNo)
  979. let qrData: any = null;
  980. try {
  981. qrData = JSON.parse(latestQr);
  982. } catch {
  983. startTransition(() => {
  984. setQrScanError(true);
  985. setQrScanSuccess(false);
  986. });
  987. return;
  988. }
  989. if (!(qrData?.stockInLineId && qrData?.itemId)) {
  990. startTransition(() => {
  991. setQrScanError(true);
  992. setQrScanSuccess(false);
  993. });
  994. return;
  995. }
  996. const scannedItemId = Number(qrData.itemId);
  997. const scannedStockInLineId = Number(qrData.stockInLineId);
  998. // ✅ avoid duplicate processing by itemId+stockInLineId
  999. const itemProcessedSet = processedQrCombinations.get(scannedItemId);
  1000. if (itemProcessedSet?.has(scannedStockInLineId)) return;
  1001. const indexes = lotDataIndexes;
  1002. const activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || [];
  1003. // ✅ Also get all lots for this item (not just active ones) to allow lot switching even when all lots are rejected
  1004. const allLotsForItem = indexes.byItemId.get(scannedItemId) || [];
  1005. // ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots
  1006. // This allows users to scan other lots even when all suggested lots are rejected
  1007. const scannedLot = allLotsForItem.find(
  1008. (lot: any) => lot.stockInLineId === scannedStockInLineId
  1009. );
  1010. if (scannedLot) {
  1011. const isRejected =
  1012. scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
  1013. scannedLot.lotAvailability === 'rejected' ||
  1014. scannedLot.lotAvailability === 'status_unavailable';
  1015. if (isRejected) {
  1016. console.warn(`⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is rejected or unavailable`);
  1017. startTransition(() => {
  1018. setQrScanError(true);
  1019. setQrScanSuccess(false);
  1020. setQrScanErrorMsg(
  1021. `此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。`
  1022. );
  1023. });
  1024. // Mark as processed to prevent re-processing
  1025. setProcessedQrCombinations(prev => {
  1026. const newMap = new Map(prev);
  1027. if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
  1028. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1029. return newMap;
  1030. });
  1031. return;
  1032. }
  1033. }
  1034. // ✅ If no active suggested lots, but scanned lot is not rejected, allow lot switching
  1035. if (activeSuggestedLots.length === 0) {
  1036. // Check if there are any lots for this item (even if all are rejected)
  1037. if (allLotsForItem.length === 0) {
  1038. console.error("No lots found for this item");
  1039. startTransition(() => {
  1040. setQrScanError(true);
  1041. setQrScanSuccess(false);
  1042. setQrScanErrorMsg("当前订单中没有此物品的批次信息");
  1043. });
  1044. return;
  1045. }
  1046. // ✅ Allow lot switching: find a rejected lot as expected lot, or use first lot
  1047. // This allows users to switch to a new lot even when all suggested lots are rejected
  1048. console.log(`⚠️ [QR PROCESS] No active suggested lots, but allowing lot switching. Scanned lot is not rejected.`);
  1049. // Find a rejected lot as expected lot (the one that was rejected)
  1050. const rejectedLot = allLotsForItem.find((lot: any) =>
  1051. lot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
  1052. lot.lotAvailability === 'rejected' ||
  1053. lot.lotAvailability === 'status_unavailable'
  1054. );
  1055. const expectedLot = rejectedLot || allLotsForItem[0]; // Use rejected lot if exists, otherwise first lot
  1056. // ✅ Always open confirmation modal when no active lots (user needs to confirm switching)
  1057. // handleLotMismatch will fetch lotNo from backend using stockInLineId if needed
  1058. console.log(`⚠️ [QR PROCESS] Opening confirmation modal for lot switch (no active lots)`);
  1059. setSelectedLotForQr(expectedLot);
  1060. handleLotMismatch(
  1061. {
  1062. lotNo: expectedLot.lotNo,
  1063. itemCode: expectedLot.itemCode,
  1064. itemName: expectedLot.itemName
  1065. },
  1066. {
  1067. lotNo: scannedLot?.lotNo || null, // Will be fetched by handleLotMismatch if null
  1068. itemCode: expectedLot.itemCode,
  1069. itemName: expectedLot.itemName,
  1070. inventoryLotLineId: scannedLot?.lotId || null,
  1071. stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo
  1072. }
  1073. );
  1074. return;
  1075. }
  1076. // ✅ direct stockInLineId match (O(1))
  1077. const stockInLineLots = indexes.byStockInLineId.get(scannedStockInLineId) || [];
  1078. let exactMatch: any = null;
  1079. for (let i = 0; i < stockInLineLots.length; i++) {
  1080. const lot = stockInLineLots[i];
  1081. if (lot.itemId === scannedItemId && activeSuggestedLots.includes(lot)) {
  1082. exactMatch = lot;
  1083. break;
  1084. }
  1085. }
  1086. console.log(`🔍 [QR PROCESS] Scanned stockInLineId: ${scannedStockInLineId}, itemId: ${scannedItemId}`);
  1087. console.log(`🔍 [QR PROCESS] Found ${stockInLineLots.length} lots with stockInLineId ${scannedStockInLineId}`);
  1088. console.log(`🔍 [QR PROCESS] Exact match found: ${exactMatch ? `YES (lotNo: ${exactMatch.lotNo}, stockOutLineId: ${exactMatch.stockOutLineId})` : 'NO'}`);
  1089. // ✅ Check if scanned lot exists in allLotsForItem but not in activeSuggestedLots
  1090. // This handles the case where Lot A is rejected and user scans Lot B
  1091. if (!exactMatch && scannedLot && !activeSuggestedLots.includes(scannedLot)) {
  1092. // Scanned lot is not in active suggested lots, open confirmation modal
  1093. const expectedLot = activeSuggestedLots[0] || allLotsForItem[0]; // Use first active lot or first lot as expected
  1094. if (expectedLot && scannedLot.stockInLineId !== expectedLot.stockInLineId) {
  1095. console.log(`⚠️ [QR PROCESS] Scanned lot ${scannedLot.lotNo} is not in active suggested lots, opening confirmation modal`);
  1096. setSelectedLotForQr(expectedLot);
  1097. handleLotMismatch(
  1098. {
  1099. lotNo: expectedLot.lotNo,
  1100. itemCode: expectedLot.itemCode,
  1101. itemName: expectedLot.itemName
  1102. },
  1103. {
  1104. lotNo: scannedLot.lotNo || null,
  1105. itemCode: expectedLot.itemCode,
  1106. itemName: expectedLot.itemName,
  1107. inventoryLotLineId: scannedLot.lotId || null,
  1108. stockInLineId: scannedStockInLineId
  1109. }
  1110. );
  1111. return;
  1112. }
  1113. }
  1114. if (exactMatch) {
  1115. if (!exactMatch.stockOutLineId) {
  1116. console.error(`❌ [QR PROCESS] Exact match found but no stockOutLineId`);
  1117. startTransition(() => {
  1118. setQrScanError(true);
  1119. setQrScanSuccess(false);
  1120. });
  1121. return;
  1122. }
  1123. console.log(`✅ [QR PROCESS] Processing exact match: lotNo=${exactMatch.lotNo}, stockOutLineId=${exactMatch.stockOutLineId}`);
  1124. try {
  1125. if (currentUserId && exactMatch.pickOrderId && exactMatch.itemId) {
  1126. try {
  1127. await updateHandledBy(exactMatch.pickOrderId, exactMatch.itemId);
  1128. console.log(`✅ [QR PROCESS] Handler updated for itemId ${exactMatch.itemId}`);
  1129. } catch (error) {
  1130. console.error(`❌ [QR PROCESS] Error updating handler (non-critical):`, error);
  1131. }
  1132. }
  1133. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  1134. pickOrderLineId: exactMatch.pickOrderLineId,
  1135. inventoryLotNo: exactMatch.lotNo,
  1136. stockOutLineId: exactMatch.stockOutLineId,
  1137. itemId: exactMatch.itemId,
  1138. status: "checked",
  1139. });
  1140. if (res.code === "checked" || res.code === "SUCCESS") {
  1141. console.log(`✅ [QR PROCESS] Successfully updated stockOutLine ${exactMatch.stockOutLineId} to checked`);
  1142. const entity = res.entity as any;
  1143. startTransition(() => {
  1144. setQrScanError(false);
  1145. setQrScanSuccess(true);
  1146. });
  1147. // mark combination processed
  1148. setProcessedQrCombinations(prev => {
  1149. const newMap = new Map(prev);
  1150. if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
  1151. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1152. return newMap;
  1153. });
  1154. // refresh to keep consistency with server & handler updates
  1155. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1156. await fetchJobOrderData(pickOrderId);
  1157. } else {
  1158. console.error(`❌ [QR PROCESS] Update failed: ${res.code}`);
  1159. startTransition(() => {
  1160. setQrScanError(true);
  1161. setQrScanSuccess(false);
  1162. });
  1163. }
  1164. } catch (error) {
  1165. console.error(`❌ [QR PROCESS] Error updating stockOutLine:`, error);
  1166. startTransition(() => {
  1167. setQrScanError(true);
  1168. setQrScanSuccess(false);
  1169. });
  1170. }
  1171. return;
  1172. }
  1173. // ✅ mismatch: validate scanned stockInLineId exists before opening confirmation modal
  1174. console.log(`⚠️ [QR PROCESS] No exact match found. Validating scanned stockInLineId ${scannedStockInLineId} for itemId ${scannedItemId}`);
  1175. console.log(`⚠️ [QR PROCESS] Active suggested lots for itemId ${scannedItemId}:`, activeSuggestedLots.map(l => ({ lotNo: l.lotNo, stockInLineId: l.stockInLineId })));
  1176. if (activeSuggestedLots.length === 0) {
  1177. console.error(`❌ [QR PROCESS] No active suggested lots found for itemId ${scannedItemId}`);
  1178. startTransition(() => {
  1179. setQrScanError(true);
  1180. setQrScanSuccess(false);
  1181. setQrScanErrorMsg(`当前订单中没有 itemId ${scannedItemId} 的可用批次`);
  1182. });
  1183. return;
  1184. }
  1185. const expectedLot = activeSuggestedLots[0];
  1186. console.log(`⚠️ [QR PROCESS] Expected lot: ${expectedLot.lotNo} (stockInLineId: ${expectedLot.stockInLineId}), Scanned stockInLineId: ${scannedStockInLineId}`);
  1187. // ✅ Validate scanned stockInLineId exists before opening modal
  1188. // This ensures the backend can find the lot when user confirms
  1189. try {
  1190. console.log(`🔍 [QR PROCESS] Validating scanned stockInLineId ${scannedStockInLineId} exists...`);
  1191. const stockInLineInfo = await fetchStockInLineInfoCached(scannedStockInLineId);
  1192. console.log(`✅ [QR PROCESS] Scanned stockInLineId ${scannedStockInLineId} exists, lotNo: ${stockInLineInfo.lotNo}`);
  1193. // ✅ 检查扫描的批次是否已被拒绝
  1194. const scannedLot = combinedLotData.find(
  1195. (lot: any) => lot.stockInLineId === scannedStockInLineId && lot.itemId === scannedItemId
  1196. );
  1197. if (scannedLot) {
  1198. const isRejected =
  1199. scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
  1200. scannedLot.lotAvailability === 'rejected' ||
  1201. scannedLot.lotAvailability === 'status_unavailable';
  1202. if (isRejected) {
  1203. console.warn(`⚠️ [QR PROCESS] Scanned lot ${stockInLineInfo.lotNo} (stockInLineId: ${scannedStockInLineId}) is rejected or unavailable`);
  1204. startTransition(() => {
  1205. setQrScanError(true);
  1206. setQrScanSuccess(false);
  1207. setQrScanErrorMsg(
  1208. `此批次(${stockInLineInfo.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。`
  1209. );
  1210. });
  1211. // Mark as processed to prevent re-processing
  1212. setProcessedQrCombinations(prev => {
  1213. const newMap = new Map(prev);
  1214. if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
  1215. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1216. return newMap;
  1217. });
  1218. return;
  1219. }
  1220. }
  1221. // ✅ stockInLineId exists and is not rejected, open confirmation modal
  1222. console.log(`⚠️ [QR PROCESS] Opening confirmation modal - user must confirm before any lot is marked as scanned`);
  1223. setSelectedLotForQr(expectedLot);
  1224. handleLotMismatch(
  1225. {
  1226. lotNo: expectedLot.lotNo,
  1227. itemCode: expectedLot.itemCode,
  1228. itemName: expectedLot.itemName
  1229. },
  1230. {
  1231. lotNo: stockInLineInfo.lotNo || null, // Use fetched lotNo for display
  1232. itemCode: expectedLot.itemCode,
  1233. itemName: expectedLot.itemName,
  1234. inventoryLotLineId: null,
  1235. stockInLineId: scannedStockInLineId
  1236. }
  1237. );
  1238. } catch (error) {
  1239. // ✅ stockInLineId does NOT exist, show error immediately (don't open modal)
  1240. console.error(`❌ [QR PROCESS] Scanned stockInLineId ${scannedStockInLineId} does NOT exist:`, error);
  1241. startTransition(() => {
  1242. setQrScanError(true);
  1243. setQrScanSuccess(false);
  1244. setQrScanErrorMsg(
  1245. `扫描的 stockInLineId ${scannedStockInLineId} 不存在。请检查 QR 码是否正确,或联系管理员。`
  1246. );
  1247. });
  1248. // Mark as processed to prevent re-processing
  1249. setProcessedQrCombinations(prev => {
  1250. const newMap = new Map(prev);
  1251. if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
  1252. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1253. return newMap;
  1254. });
  1255. }
  1256. }, [filterArgs?.pickOrderId, fetchJobOrderData, handleLotMismatch, lotDataIndexes, processedQrCombinations, combinedLotData, fetchStockInLineInfoCached,currentUserId, updateHandledBy ]);
  1257. // Store in refs for immediate access in qrValues effect
  1258. processOutsideQrCodeRef.current = processOutsideQrCode;
  1259. resetScanRef.current = resetScan;
  1260. const handleManualInputSubmit = useCallback(() => {
  1261. if (qrScanInput.trim() !== '') {
  1262. handleQrCodeSubmit(qrScanInput.trim());
  1263. }
  1264. }, [qrScanInput, handleQrCodeSubmit]);
  1265. // Handle QR code submission from modal (internal scanning)
  1266. const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
  1267. if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
  1268. console.log(` QR Code verified for lot: ${lotNo}`);
  1269. const requiredQty = selectedLotForQr.requiredQty;
  1270. const lotId = selectedLotForQr.lotId;
  1271. // Create stock out line
  1272. const stockOutLineData: CreateStockOutLine = {
  1273. consoCode: selectedLotForQr.pickOrderConsoCode,
  1274. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  1275. inventoryLotLineId: selectedLotForQr.lotId,
  1276. qty: 0.0
  1277. };
  1278. try {
  1279. await createStockOutLine(stockOutLineData);
  1280. console.log("Stock out line created successfully!");
  1281. // Close modal
  1282. setQrModalOpen(false);
  1283. setSelectedLotForQr(null);
  1284. // Set pick quantity
  1285. const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`;
  1286. setTimeout(() => {
  1287. setPickQtyData(prev => ({
  1288. ...prev,
  1289. [lotKey]: requiredQty
  1290. }));
  1291. console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
  1292. }, 500);
  1293. // Refresh data
  1294. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1295. await fetchJobOrderData(pickOrderId);
  1296. } catch (error) {
  1297. console.error("Error creating stock out line:", error);
  1298. }
  1299. }
  1300. }, [selectedLotForQr, fetchJobOrderData]);
  1301. useEffect(() => {
  1302. // Skip if scanner not active or no data or currently refreshing
  1303. if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) return;
  1304. const latestQr = qrValues[qrValues.length - 1];
  1305. // ✅ Test shortcut: {2fitestx,y} or {2fittestx,y} where x=itemId, y=stockInLineId
  1306. if ((latestQr.startsWith("{2fitest") || latestQr.startsWith("{2fittest")) && latestQr.endsWith("}")) {
  1307. let content = '';
  1308. if (latestQr.startsWith("{2fittest")) content = latestQr.substring(9, latestQr.length - 1);
  1309. else content = latestQr.substring(8, latestQr.length - 1);
  1310. const parts = content.split(',');
  1311. if (parts.length === 2) {
  1312. const itemId = parseInt(parts[0].trim(), 10);
  1313. const stockInLineId = parseInt(parts[1].trim(), 10);
  1314. if (!isNaN(itemId) && !isNaN(stockInLineId)) {
  1315. const simulatedQr = JSON.stringify({ itemId, stockInLineId });
  1316. lastProcessedQrRef.current = latestQr;
  1317. processedQrCodesRef.current.add(latestQr);
  1318. setLastProcessedQr(latestQr);
  1319. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1320. processOutsideQrCodeRef.current?.(simulatedQr);
  1321. resetScanRef.current?.();
  1322. return;
  1323. }
  1324. }
  1325. }
  1326. // ✅ Shortcut: {2fic} open manual lot confirmation modal
  1327. if (latestQr === "{2fic}") {
  1328. setManualLotConfirmationOpen(true);
  1329. resetScanRef.current?.();
  1330. lastProcessedQrRef.current = latestQr;
  1331. processedQrCodesRef.current.add(latestQr);
  1332. setLastProcessedQr(latestQr);
  1333. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1334. return;
  1335. }
  1336. // Skip processing if modal open for same QR
  1337. if (lotConfirmationOpen || manualLotConfirmationOpen) {
  1338. if (latestQr === lastProcessedQrRef.current) return;
  1339. }
  1340. // Skip if already processed (refs)
  1341. if (processedQrCodesRef.current.has(latestQr) || lastProcessedQrRef.current === latestQr) return;
  1342. // Mark processed immediately
  1343. lastProcessedQrRef.current = latestQr;
  1344. processedQrCodesRef.current.add(latestQr);
  1345. if (processedQrCodesRef.current.size > 100) {
  1346. const firstValue = processedQrCodesRef.current.values().next().value;
  1347. if (firstValue !== undefined) processedQrCodesRef.current.delete(firstValue);
  1348. }
  1349. // Process immediately
  1350. if (qrProcessingTimeoutRef.current) {
  1351. clearTimeout(qrProcessingTimeoutRef.current);
  1352. qrProcessingTimeoutRef.current = null;
  1353. }
  1354. processOutsideQrCodeRef.current?.(latestQr);
  1355. // UI state updates (non-blocking)
  1356. startTransition(() => {
  1357. setLastProcessedQr(latestQr);
  1358. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1359. });
  1360. return () => {
  1361. if (qrProcessingTimeoutRef.current) {
  1362. clearTimeout(qrProcessingTimeoutRef.current);
  1363. qrProcessingTimeoutRef.current = null;
  1364. }
  1365. };
  1366. }, [qrValues.length, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen]);
  1367. const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
  1368. if (value === '' || value === null || value === undefined) {
  1369. setPickQtyData(prev => ({
  1370. ...prev,
  1371. [lotKey]: 0
  1372. }));
  1373. return;
  1374. }
  1375. const numericValue = typeof value === 'string' ? parseFloat(value) : value;
  1376. if (isNaN(numericValue)) {
  1377. setPickQtyData(prev => ({
  1378. ...prev,
  1379. [lotKey]: 0
  1380. }));
  1381. return;
  1382. }
  1383. setPickQtyData(prev => ({
  1384. ...prev,
  1385. [lotKey]: numericValue
  1386. }));
  1387. }, []);
  1388. const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle');
  1389. const [autoAssignMessage, setAutoAssignMessage] = useState<string>('');
  1390. const [completionStatus, setCompletionStatus] = useState<PickOrderCompletionResponse | null>(null);
  1391. const checkAndAutoAssignNext = useCallback(async () => {
  1392. if (!currentUserId) return;
  1393. try {
  1394. const completionResponse = await checkPickOrderCompletion(currentUserId);
  1395. if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) {
  1396. console.log("Found completed pick orders, auto-assigning next...");
  1397. // 移除前端的自动分配逻辑,因为后端已经处理了
  1398. // await handleAutoAssignAndRelease(); // 删除这个函数
  1399. }
  1400. } catch (error) {
  1401. console.error("Error checking pick order completion:", error);
  1402. }
  1403. }, [currentUserId]);
  1404. // Handle submit pick quantity
  1405. const handleSubmitPickQty = useCallback(async (lot: any) => {
  1406. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  1407. const newQty = pickQtyData[lotKey] || 0;
  1408. if (!lot.stockOutLineId) {
  1409. console.error("No stock out line found for this lot");
  1410. return;
  1411. }
  1412. try {
  1413. const currentActualPickQty = lot.actualPickQty || 0;
  1414. const cumulativeQty = currentActualPickQty + newQty;
  1415. let newStatus = 'partially_completed';
  1416. if (cumulativeQty >= lot.requiredQty) {
  1417. newStatus = 'completed';
  1418. }
  1419. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  1420. console.log(`Lot: ${lot.lotNo}`);
  1421. console.log(`Required Qty: ${lot.requiredQty}`);
  1422. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  1423. console.log(`New Submitted Qty: ${newQty}`);
  1424. console.log(`Cumulative Qty: ${cumulativeQty}`);
  1425. console.log(`New Status: ${newStatus}`);
  1426. console.log(`=====================================`);
  1427. await updateStockOutLineStatus({
  1428. id: lot.stockOutLineId,
  1429. status: newStatus,
  1430. qty: cumulativeQty
  1431. });
  1432. if (newQty > 0) {
  1433. await updateInventoryLotLineQuantities({
  1434. inventoryLotLineId: lot.lotId,
  1435. qty: newQty,
  1436. status: 'available',
  1437. operation: 'pick'
  1438. });
  1439. }
  1440. // FIXED: Use the proper API function instead of direct fetch
  1441. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  1442. console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  1443. try {
  1444. // Use the imported API function instead of direct fetch
  1445. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  1446. console.log(` Pick order completion check result:`, completionResponse);
  1447. if (completionResponse.code === "SUCCESS") {
  1448. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  1449. } else if (completionResponse.message === "not completed") {
  1450. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  1451. } else {
  1452. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  1453. }
  1454. } catch (error) {
  1455. console.error("Error checking pick order completion:", error);
  1456. }
  1457. }
  1458. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1459. await fetchJobOrderData(pickOrderId);
  1460. console.log("Pick quantity submitted successfully!");
  1461. setTimeout(() => {
  1462. checkAndAutoAssignNext();
  1463. }, 1000);
  1464. } catch (error) {
  1465. console.error("Error submitting pick quantity:", error);
  1466. }
  1467. }, [pickQtyData, fetchJobOrderData, checkAndAutoAssignNext]);
  1468. const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
  1469. if (!lot.stockOutLineId) {
  1470. console.error("No stock out line found for this lot");
  1471. return;
  1472. }
  1473. try {
  1474. if (currentUserId && lot.pickOrderId && lot.itemId) {
  1475. try {
  1476. await updateHandledBy(lot.pickOrderId, lot.itemId);
  1477. } catch (error) {
  1478. console.error("❌ Error updating handler (non-critical):", error);
  1479. // Continue even if handler update fails
  1480. }
  1481. }
  1482. // Special case: If submitQty is 0 and all values are 0, mark as completed with qty: 0
  1483. if (submitQty === 0) {
  1484. console.log(`=== SUBMITTING ALL ZEROS CASE ===`);
  1485. console.log(`Lot: ${lot.lotNo}`);
  1486. console.log(`Stock Out Line ID: ${lot.stockOutLineId}`);
  1487. console.log(`Setting status to 'completed' with qty: 0`);
  1488. const updateResult = await updateStockOutLineStatus({
  1489. id: lot.stockOutLineId,
  1490. status: 'completed',
  1491. qty: 0
  1492. });
  1493. console.log('Update result:', updateResult);
  1494. const r: any = updateResult as any;
  1495. const updateOk =
  1496. r?.code === 'SUCCESS' ||
  1497. r?.type === 'completed' ||
  1498. typeof r?.id === 'number' ||
  1499. typeof r?.entity?.id === 'number' ||
  1500. (r?.message && r.message.includes('successfully'));
  1501. if (!updateResult || !updateOk) {
  1502. console.error('Failed to update stock out line status:', updateResult);
  1503. throw new Error('Failed to update stock out line status');
  1504. }
  1505. // Check if pick order is completed
  1506. if (lot.pickOrderConsoCode) {
  1507. console.log(` Lot ${lot.lotNo} completed (all zeros), checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  1508. try {
  1509. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  1510. console.log(` Pick order completion check result:`, completionResponse);
  1511. if (completionResponse.code === "SUCCESS") {
  1512. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  1513. setTimeout(() => {
  1514. if (onBackToList) {
  1515. onBackToList();
  1516. }
  1517. }, 1500);
  1518. } else if (completionResponse.message === "not completed") {
  1519. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  1520. } else {
  1521. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  1522. }
  1523. } catch (error) {
  1524. console.error("Error checking pick order completion:", error);
  1525. }
  1526. }
  1527. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1528. await fetchJobOrderData(pickOrderId);
  1529. console.log("All zeros submission completed successfully!");
  1530. setTimeout(() => {
  1531. checkAndAutoAssignNext();
  1532. }, 1000);
  1533. return;
  1534. }
  1535. // Normal case: Calculate cumulative quantity correctly
  1536. const currentActualPickQty = lot.actualPickQty || 0;
  1537. const cumulativeQty = currentActualPickQty + submitQty;
  1538. // Determine status based on cumulative quantity vs required quantity
  1539. let newStatus = 'partially_completed';
  1540. if (cumulativeQty >= lot.requiredQty) {
  1541. newStatus = 'completed';
  1542. } else if (cumulativeQty > 0) {
  1543. newStatus = 'partially_completed';
  1544. } else {
  1545. newStatus = 'checked'; // QR scanned but no quantity submitted yet
  1546. }
  1547. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  1548. console.log(`Lot: ${lot.lotNo}`);
  1549. console.log(`Required Qty: ${lot.requiredQty}`);
  1550. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  1551. console.log(`New Submitted Qty: ${submitQty}`);
  1552. console.log(`Cumulative Qty: ${cumulativeQty}`);
  1553. console.log(`New Status: ${newStatus}`);
  1554. console.log(`=====================================`);
  1555. await updateStockOutLineStatus({
  1556. id: lot.stockOutLineId,
  1557. status: newStatus,
  1558. qty: cumulativeQty
  1559. });
  1560. if (submitQty > 0) {
  1561. await updateInventoryLotLineQuantities({
  1562. inventoryLotLineId: lot.lotId,
  1563. qty: submitQty,
  1564. status: 'available',
  1565. operation: 'pick'
  1566. });
  1567. }
  1568. // Check if pick order is completed when lot status becomes 'completed'
  1569. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  1570. console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  1571. try {
  1572. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  1573. console.log(` Pick order completion check result:`, completionResponse);
  1574. if (completionResponse.code === "SUCCESS") {
  1575. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  1576. setTimeout(() => {
  1577. if (onBackToList) {
  1578. onBackToList();
  1579. }
  1580. }, 1500);
  1581. } else if (completionResponse.message === "not completed") {
  1582. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  1583. } else {
  1584. console.error(`❌ Error checking completion: ${completionResponse.message}`);
  1585. }
  1586. } catch (error) {
  1587. console.error("Error checking pick order completion:", error);
  1588. }
  1589. }
  1590. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1591. await fetchJobOrderData(pickOrderId);
  1592. console.log("Pick quantity submitted successfully!");
  1593. setTimeout(() => {
  1594. checkAndAutoAssignNext();
  1595. }, 1000);
  1596. } catch (error) {
  1597. console.error("Error submitting pick quantity:", error);
  1598. }
  1599. }, [fetchJobOrderData, checkAndAutoAssignNext, filterArgs]);
  1600. const handleSkip = useCallback(async (lot: any) => {
  1601. try {
  1602. console.log("Skip clicked, submit 0 qty for lot:", lot.lotNo);
  1603. await handleSubmitPickQtyWithQty(lot, 0);
  1604. } catch (err) {
  1605. console.error("Error in Skip:", err);
  1606. }
  1607. }, [handleSubmitPickQtyWithQty]);
  1608. const handleSubmitAllScanned = useCallback(async () => {
  1609. const scannedLots = combinedLotData.filter(lot =>
  1610. lot.stockOutLineStatus === 'checked'
  1611. );
  1612. if (scannedLots.length === 0) {
  1613. console.log("No scanned items to submit");
  1614. return;
  1615. }
  1616. setIsSubmittingAll(true);
  1617. console.log(`📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`);
  1618. try {
  1619. // ✅ 批量更新所有相关行的 handler(在提交前)
  1620. if (currentUserId) {
  1621. const uniqueItemIds = new Set(scannedLots.map(lot => lot.itemId));
  1622. const updatePromises = Array.from(uniqueItemIds).map(itemId => {
  1623. const lot = scannedLots.find(l => l.itemId === itemId);
  1624. if (lot && lot.pickOrderId) {
  1625. return updateHandledBy(lot.pickOrderId, itemId).catch(err => {
  1626. console.error(`❌ Error updating handler for itemId ${itemId}:`, err);
  1627. });
  1628. }
  1629. return Promise.resolve();
  1630. });
  1631. await Promise.all(updatePromises);
  1632. console.log(`✅ Updated handlers for ${uniqueItemIds.size} unique items`);
  1633. }
  1634. // ✅ 转换为 batchSubmitList 所需的格式
  1635. const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => {
  1636. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty || 0;
  1637. const currentActualPickQty = lot.actualPickQty || 0;
  1638. const cumulativeQty = currentActualPickQty + submitQty;
  1639. let newStatus = 'partially_completed';
  1640. if (cumulativeQty >= (lot.requiredQty || 0)) {
  1641. newStatus = 'completed';
  1642. }
  1643. return {
  1644. stockOutLineId: Number(lot.stockOutLineId) || 0,
  1645. pickOrderLineId: Number(lot.pickOrderLineId),
  1646. inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null,
  1647. requiredQty: Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0),
  1648. actualPickQty: Number(cumulativeQty),
  1649. stockOutLineStatus: newStatus,
  1650. pickOrderConsoCode: String(lot.pickOrderConsoCode || ''),
  1651. noLot: Boolean(false) // Job Order 通常都有 lot
  1652. };
  1653. });
  1654. const request: batchSubmitListRequest = {
  1655. userId: currentUserId || 0,
  1656. lines: lines
  1657. };
  1658. // ✅ 使用 batchSubmitList API
  1659. const result = await batchSubmitList(request);
  1660. console.log(`📥 Batch submit result:`, result);
  1661. // 刷新数据
  1662. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1663. await fetchJobOrderData(pickOrderId);
  1664. if (result && result.code === "SUCCESS") {
  1665. setQrScanSuccess(true);
  1666. setTimeout(() => {
  1667. setQrScanSuccess(false);
  1668. checkAndAutoAssignNext();
  1669. if (onBackToList) {
  1670. onBackToList();
  1671. }
  1672. }, 2000);
  1673. } else {
  1674. console.error("Batch submit failed:", result);
  1675. setQrScanError(true);
  1676. }
  1677. } catch (error) {
  1678. console.error("Error submitting all scanned items:", error);
  1679. setQrScanError(true);
  1680. } finally {
  1681. setIsSubmittingAll(false);
  1682. }
  1683. }, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId, onBackToList, updateHandledBy])
  1684. const scannedItemsCount = useMemo(() => {
  1685. return combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked').length;
  1686. }, [combinedLotData]);
  1687. // Progress bar data (align with Finished Good execution detail)
  1688. const progress = useMemo(() => {
  1689. if (combinedLotData.length === 0) {
  1690. return { completed: 0, total: 0 };
  1691. }
  1692. const nonPendingCount = combinedLotData.filter((lot) => {
  1693. const status = lot.stockOutLineStatus?.toLowerCase();
  1694. return status !== 'pending';
  1695. }).length;
  1696. return {
  1697. completed: nonPendingCount,
  1698. total: combinedLotData.length,
  1699. };
  1700. }, [combinedLotData]);
  1701. // Handle reject lot
  1702. const handleRejectLot = useCallback(async (lot: any) => {
  1703. if (!lot.stockOutLineId) {
  1704. console.error("No stock out line found for this lot");
  1705. return;
  1706. }
  1707. try {
  1708. await updateStockOutLineStatus({
  1709. id: lot.stockOutLineId,
  1710. status: 'rejected',
  1711. qty: 0
  1712. });
  1713. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1714. await fetchJobOrderData(pickOrderId);
  1715. console.log("Lot rejected successfully!");
  1716. setTimeout(() => {
  1717. checkAndAutoAssignNext();
  1718. }, 1000);
  1719. } catch (error) {
  1720. console.error("Error rejecting lot:", error);
  1721. }
  1722. }, [fetchJobOrderData, checkAndAutoAssignNext]);
  1723. // Handle pick execution form
  1724. const handlePickExecutionForm = useCallback((lot: any) => {
  1725. console.log("=== Pick Execution Form ===");
  1726. console.log("Lot data:", lot);
  1727. if (!lot) {
  1728. console.warn("No lot data provided for pick execution form");
  1729. return;
  1730. }
  1731. console.log("Opening pick execution form for lot:", lot.lotNo);
  1732. setSelectedLotForExecutionForm(lot);
  1733. setPickExecutionFormOpen(true);
  1734. console.log("Pick execution form opened for lot ID:", lot.lotId);
  1735. }, []);
  1736. const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
  1737. try {
  1738. if (currentUserId && selectedLotForExecutionForm?.pickOrderId && selectedLotForExecutionForm?.itemId) {
  1739. try {
  1740. await updateHandledBy(selectedLotForExecutionForm.pickOrderId, selectedLotForExecutionForm.itemId);
  1741. console.log(`✅ [ISSUE FORM] Handler updated for itemId ${selectedLotForExecutionForm.itemId}`);
  1742. } catch (error) {
  1743. console.error(`❌ [ISSUE FORM] Error updating handler (non-critical):`, error);
  1744. }
  1745. }
  1746. console.log("Pick execution form submitted:", data);
  1747. const issueData = {
  1748. ...data,
  1749. type: "Jo", // Delivery Order Record 类型
  1750. };
  1751. const result = await recordPickExecutionIssue(issueData);
  1752. console.log("Pick execution issue recorded:", result);
  1753. if (result && result.code === "SUCCESS") {
  1754. console.log(" Pick execution issue recorded successfully");
  1755. } else {
  1756. console.error("❌ Failed to record pick execution issue:", result);
  1757. }
  1758. setPickExecutionFormOpen(false);
  1759. setSelectedLotForExecutionForm(null);
  1760. const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
  1761. await fetchJobOrderData(pickOrderId);
  1762. } catch (error) {
  1763. console.error("Error submitting pick execution form:", error);
  1764. }
  1765. }, [fetchJobOrderData]);
  1766. // Calculate remaining required quantity
  1767. const calculateRemainingRequiredQty = useCallback((lot: any) => {
  1768. const requiredQty = lot.requiredQty || 0;
  1769. const stockOutLineQty = lot.stockOutLineQty || 0;
  1770. return Math.max(0, requiredQty - stockOutLineQty);
  1771. }, []);
  1772. // Search criteria
  1773. const searchCriteria: Criterion<any>[] = [
  1774. {
  1775. label: t("Pick Order Code"),
  1776. paramName: "pickOrderCode",
  1777. type: "text",
  1778. },
  1779. {
  1780. label: t("Item Code"),
  1781. paramName: "itemCode",
  1782. type: "text",
  1783. },
  1784. {
  1785. label: t("Item Name"),
  1786. paramName: "itemName",
  1787. type: "text",
  1788. },
  1789. {
  1790. label: t("Lot No"),
  1791. paramName: "lotNo",
  1792. type: "text",
  1793. },
  1794. ];
  1795. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  1796. setPaginationController(prev => ({
  1797. ...prev,
  1798. pageNum: newPage,
  1799. }));
  1800. }, []);
  1801. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  1802. const newPageSize = parseInt(event.target.value, 10);
  1803. setPaginationController({
  1804. pageNum: 0,
  1805. pageSize: newPageSize,
  1806. });
  1807. }, []);
  1808. // Pagination data with sorting by routerIndex
  1809. const paginatedData = useMemo(() => {
  1810. // Sort by routerIndex first, then by other criteria
  1811. const sortedData = [...combinedLotData].sort((a, b) => {
  1812. const aIndex = a.routerIndex || 0;
  1813. const bIndex = b.routerIndex || 0;
  1814. // Primary sort: by routerIndex
  1815. if (aIndex !== bIndex) {
  1816. return aIndex - bIndex;
  1817. }
  1818. // Secondary sort: by pickOrderCode if routerIndex is the same
  1819. if (a.pickOrderCode !== b.pickOrderCode) {
  1820. return a.pickOrderCode.localeCompare(b.pickOrderCode);
  1821. }
  1822. // Tertiary sort: by lotNo if everything else is the same
  1823. return (a.lotNo || '').localeCompare(b.lotNo || '');
  1824. });
  1825. const startIndex = paginationController.pageNum * paginationController.pageSize;
  1826. const endIndex = startIndex + paginationController.pageSize;
  1827. return sortedData.slice(startIndex, endIndex);
  1828. }, [combinedLotData, paginationController]);
  1829. // Add these functions for manual scanning
  1830. const handleStartScan = useCallback(() => {
  1831. console.log(" Starting manual QR scan...");
  1832. setIsManualScanning(true);
  1833. setProcessedQrCodes(new Set());
  1834. setLastProcessedQr('');
  1835. setQrScanError(false);
  1836. setQrScanSuccess(false);
  1837. startScan();
  1838. }, [startScan]);
  1839. const handleStopScan = useCallback(() => {
  1840. console.log(" Stopping manual QR scan...");
  1841. setIsManualScanning(false);
  1842. setQrScanError(false);
  1843. setQrScanSuccess(false);
  1844. stopScan();
  1845. resetScan();
  1846. }, [stopScan, resetScan]);
  1847. useEffect(() => {
  1848. return () => {
  1849. // Cleanup when component unmounts (e.g., when switching tabs)
  1850. if (isManualScanning) {
  1851. console.log("🧹 Component unmounting, stopping QR scanner...");
  1852. stopScan();
  1853. resetScan();
  1854. }
  1855. };
  1856. }, [isManualScanning, stopScan, resetScan]);
  1857. useEffect(() => {
  1858. if (isManualScanning && combinedLotData.length === 0) {
  1859. console.log(" No data available, auto-stopping QR scan...");
  1860. handleStopScan();
  1861. }
  1862. }, [combinedLotData.length, isManualScanning, handleStopScan]);
  1863. // Cleanup effect
  1864. useEffect(() => {
  1865. return () => {
  1866. // Cleanup when component unmounts (e.g., when switching tabs)
  1867. if (isManualScanning) {
  1868. console.log("🧹 Component unmounting, stopping QR scanner...");
  1869. stopScan();
  1870. resetScan();
  1871. }
  1872. };
  1873. }, [isManualScanning, stopScan, resetScan]);
  1874. const getStatusMessage = useCallback((lot: any) => {
  1875. switch (lot.stockOutLineStatus?.toLowerCase()) {
  1876. case 'pending':
  1877. return t("Please finish QR code scan and pick order.");
  1878. case 'checked':
  1879. return t("Please submit the pick order.");
  1880. case 'partially_completed':
  1881. return t("Partial quantity submitted. Please submit more or complete the order.");
  1882. case 'completed':
  1883. return t("Pick order completed successfully!");
  1884. case 'rejected':
  1885. return t("Lot has been rejected and marked as unavailable.");
  1886. case 'unavailable':
  1887. return t("This order is insufficient, please pick another lot.");
  1888. default:
  1889. return t("Please finish QR code scan and pick order.");
  1890. }
  1891. }, [t]);
  1892. return (
  1893. <TestQrCodeProvider
  1894. lotData={combinedLotData}
  1895. onScanLot={handleQrCodeSubmit}
  1896. filterActive={(lot) => (
  1897. lot.lotAvailability !== 'rejected' &&
  1898. lot.stockOutLineStatus !== 'rejected' &&
  1899. lot.stockOutLineStatus !== 'completed'
  1900. )}
  1901. >
  1902. <FormProvider {...formProps}>
  1903. <Stack spacing={2}>
  1904. {/* Progress bar + scan status fixed at top */}
  1905. <Box
  1906. sx={{
  1907. position: 'fixed',
  1908. top: 0,
  1909. left: 0,
  1910. right: 0,
  1911. zIndex: 1100,
  1912. backgroundColor: 'background.paper',
  1913. pt: 2,
  1914. pb: 1,
  1915. px: 2,
  1916. borderBottom: '1px solid',
  1917. borderColor: 'divider',
  1918. boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
  1919. }}
  1920. >
  1921. <LinearProgressWithLabel
  1922. completed={progress.completed}
  1923. total={progress.total}
  1924. label={t("Progress")}
  1925. />
  1926. <ScanStatusAlert
  1927. error={qrScanError}
  1928. success={qrScanSuccess}
  1929. errorMessage={qrScanErrorMsg || t("QR code does not match any item in current orders.")}
  1930. successMessage={t("QR code verified.")}
  1931. />
  1932. </Box>
  1933. {/* Job Order Header */}
  1934. {jobOrderData && (
  1935. <Paper sx={{ p: 2 }}>
  1936. <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
  1937. <Typography variant="subtitle1">
  1938. <strong>{t("Job Order")}:</strong> {jobOrderData.pickOrder?.jobOrder?.code || '-'}
  1939. </Typography>
  1940. <Typography variant="subtitle1">
  1941. <strong>{t("Pick Order Code")}:</strong> {jobOrderData.pickOrder?.code || '-'}
  1942. </Typography>
  1943. <Typography variant="subtitle1">
  1944. <strong>{t("Target Date")}:</strong> {jobOrderData.pickOrder?.targetDate || '-'}
  1945. </Typography>
  1946. </Stack>
  1947. </Paper>
  1948. )}
  1949. {/* Combined Lot Table */}
  1950. <Box sx={{ mt: 10 }}>
  1951. <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
  1952. <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
  1953. {!isManualScanning ? (
  1954. <Button
  1955. variant="contained"
  1956. startIcon={<QrCodeIcon />}
  1957. onClick={handleStartScan}
  1958. color="primary"
  1959. sx={{ minWidth: '120px' }}
  1960. >
  1961. {t("Start QR Scan")}
  1962. </Button>
  1963. ) : (
  1964. <Button
  1965. variant="outlined"
  1966. startIcon={<QrCodeIcon />}
  1967. onClick={handleStopScan}
  1968. color="secondary"
  1969. sx={{ minWidth: '120px' }}
  1970. >
  1971. {t("Stop QR Scan")}
  1972. </Button>
  1973. )}
  1974. {/* ADD THIS: Submit All Scanned Button */}
  1975. <Button
  1976. variant="contained"
  1977. color="success"
  1978. onClick={handleSubmitAllScanned}
  1979. disabled={scannedItemsCount === 0 || isSubmittingAll}
  1980. sx={{ minWidth: '160px' }}
  1981. >
  1982. {isSubmittingAll ? (
  1983. <>
  1984. <CircularProgress size={16} sx={{ mr: 1 }} />
  1985. {t("Submitting...")}
  1986. </>
  1987. ) : (
  1988. `${t("Submit All Scanned")} (${scannedItemsCount})`
  1989. )}
  1990. </Button>
  1991. </Box>
  1992. </Box>
  1993. <TableContainer component={Paper}>
  1994. <Table>
  1995. <TableHead>
  1996. <TableRow>
  1997. <TableCell>{t("Index")}</TableCell>
  1998. <TableCell>{t("Route")}</TableCell>
  1999. <TableCell>{t("Handler")}</TableCell>
  2000. <TableCell>{t("Item Code")}</TableCell>
  2001. <TableCell>{t("Item Name")}</TableCell>
  2002. <TableCell>{t("Lot No")}</TableCell>
  2003. <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
  2004. <TableCell align="center">{t("Scan Result")}</TableCell>
  2005. <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
  2006. </TableRow>
  2007. </TableHead>
  2008. <TableBody>
  2009. {paginatedData.length === 0 ? (
  2010. <TableRow>
  2011. <TableCell colSpan={8} align="center">
  2012. <Typography variant="body2" color="text.secondary">
  2013. {t("No data available")}
  2014. </Typography>
  2015. </TableCell>
  2016. </TableRow>
  2017. ) : (
  2018. paginatedData.map((lot, index) => (
  2019. <TableRow
  2020. key={`${lot.pickOrderLineId}-${lot.lotId}`}
  2021. sx={{
  2022. // backgroundColor: lot.lotAvailability === 'rejected' ? 'grey.100' : 'inherit',
  2023. //opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1,
  2024. '& .MuiTableCell-root': {
  2025. // color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit'
  2026. }
  2027. }}
  2028. >
  2029. <TableCell>
  2030. <Typography variant="body2" fontWeight="bold">
  2031. {index + 1}
  2032. </Typography>
  2033. </TableCell>
  2034. <TableCell>
  2035. <Typography variant="body2">
  2036. {lot.routerRoute || '-'}
  2037. </Typography>
  2038. </TableCell>
  2039. <TableCell>{lot.handler || '-'}</TableCell>
  2040. <TableCell>{lot.itemCode}</TableCell>
  2041. <TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell>
  2042. <TableCell>
  2043. <Box>
  2044. <Typography
  2045. sx={{
  2046. // color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit',
  2047. //opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1
  2048. }}
  2049. >
  2050. {lot.lotNo}
  2051. </Typography>
  2052. </Box>
  2053. </TableCell>
  2054. <TableCell align="right">
  2055. {(() => {
  2056. const requiredQty = lot.requiredQty || 0;
  2057. return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')';
  2058. })()}
  2059. </TableCell>
  2060. <TableCell align="center">
  2061. {(() => {
  2062. const status = lot.stockOutLineStatus?.toLowerCase();
  2063. const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
  2064. const isNoLot = !lot.lotNo;
  2065. // ✅ rejected lot:显示红色勾选(已扫描但被拒绝)
  2066. if (isRejected && !isNoLot) {
  2067. return (
  2068. <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  2069. <Checkbox
  2070. checked={true}
  2071. disabled={true}
  2072. readOnly={true}
  2073. size="large"
  2074. sx={{
  2075. color: 'error.main',
  2076. '&.Mui-checked': { color: 'error.main' },
  2077. transform: 'scale(1.3)',
  2078. }}
  2079. />
  2080. </Box>
  2081. );
  2082. }
  2083. // ✅ 正常 lot:已扫描(checked/partially_completed/completed)
  2084. if (!isNoLot && status !== 'pending' && status !== 'rejected') {
  2085. return (
  2086. <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  2087. <Checkbox
  2088. checked={true}
  2089. disabled={true}
  2090. readOnly={true}
  2091. size="large"
  2092. sx={{
  2093. color: 'success.main',
  2094. '&.Mui-checked': { color: 'success.main' },
  2095. transform: 'scale(1.3)',
  2096. }}
  2097. />
  2098. </Box>
  2099. );
  2100. }
  2101. return null;
  2102. })()}
  2103. </TableCell>
  2104. <TableCell align="center">
  2105. <Box sx={{ display: 'flex', justifyContent: 'center' }}>
  2106. {(() => {
  2107. const status = lot.stockOutLineStatus?.toLowerCase();
  2108. const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
  2109. const isNoLot = !lot.lotNo;
  2110. // ✅ rejected lot:显示提示文本(换行显示)
  2111. if (isRejected && !isNoLot) {
  2112. return (
  2113. <Typography
  2114. variant="body2"
  2115. color="error.main"
  2116. sx={{
  2117. textAlign: 'center',
  2118. whiteSpace: 'normal',
  2119. wordBreak: 'break-word',
  2120. maxWidth: '200px',
  2121. lineHeight: 1.5
  2122. }}
  2123. >
  2124. {t("This lot is rejected, please scan another lot.")}
  2125. </Typography>
  2126. );
  2127. }
  2128. // 正常 lot:显示按钮
  2129. return (
  2130. <Stack direction="row" spacing={1} alignItems="center">
  2131. <Button
  2132. variant="contained"
  2133. onClick={async () => {
  2134. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  2135. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
  2136. handlePickQtyChange(lotKey, submitQty);
  2137. await handleSubmitPickQtyWithQty(lot, submitQty);
  2138. }}
  2139. disabled={
  2140. (lot.lotAvailability === 'expired' ||
  2141. lot.lotAvailability === 'status_unavailable' ||
  2142. lot.lotAvailability === 'rejected') ||
  2143. lot.stockOutLineStatus === 'completed' ||
  2144. lot.stockOutLineStatus === 'pending'
  2145. }
  2146. sx={{
  2147. fontSize: '0.75rem',
  2148. py: 0.5,
  2149. minHeight: '28px',
  2150. minWidth: '70px'
  2151. }}
  2152. >
  2153. {t("Submit")}
  2154. </Button>
  2155. <Button
  2156. variant="outlined"
  2157. size="small"
  2158. onClick={() => handlePickExecutionForm(lot)}
  2159. disabled={
  2160. lot.stockOutLineStatus === 'completed'
  2161. }
  2162. sx={{
  2163. fontSize: '0.7rem',
  2164. py: 0.5,
  2165. minHeight: '28px',
  2166. minWidth: '60px',
  2167. borderColor: 'warning.main',
  2168. color: 'warning.main'
  2169. }}
  2170. title="Report missing or bad items"
  2171. >
  2172. {t("Edit")}
  2173. </Button>
  2174. <Button
  2175. variant="outlined"
  2176. size="small"
  2177. onClick={async () => {
  2178. // ✅ 更新 handler 后再提交
  2179. if (currentUserId && lot.pickOrderId && lot.itemId) {
  2180. try {
  2181. await updateHandledBy(lot.pickOrderId, lot.itemId);
  2182. } catch (error) {
  2183. console.error("❌ Error updating handler (non-critical):", error);
  2184. }
  2185. }
  2186. await handleSubmitPickQtyWithQty(lot, lot.requiredQty || lot.pickOrderLineRequiredQty || 0);
  2187. }}
  2188. disabled={lot.stockOutLineStatus === 'completed'}
  2189. sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '90px' }}
  2190. >
  2191. {t("Just Complete")}
  2192. </Button>
  2193. </Stack>
  2194. );
  2195. })()}
  2196. </Box>
  2197. </TableCell>
  2198. </TableRow>
  2199. ))
  2200. )}
  2201. </TableBody>
  2202. </Table>
  2203. </TableContainer>
  2204. <TablePagination
  2205. component="div"
  2206. count={combinedLotData.length}
  2207. page={paginationController.pageNum}
  2208. rowsPerPage={paginationController.pageSize}
  2209. onPageChange={handlePageChange}
  2210. onRowsPerPageChange={handlePageSizeChange}
  2211. rowsPerPageOptions={[10, 25, 50]}
  2212. labelRowsPerPage={t("Rows per page")}
  2213. labelDisplayedRows={({ from, to, count }) =>
  2214. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  2215. }
  2216. />
  2217. </Box>
  2218. </Stack>
  2219. {/* QR Code Modal */}
  2220. {!lotConfirmationOpen && (
  2221. <QrCodeModal
  2222. open={qrModalOpen}
  2223. onClose={() => {
  2224. setQrModalOpen(false);
  2225. setSelectedLotForQr(null);
  2226. stopScan();
  2227. resetScan();
  2228. }}
  2229. lot={selectedLotForQr}
  2230. combinedLotData={combinedLotData}
  2231. onQrCodeSubmit={handleQrCodeSubmitFromModal}
  2232. />
  2233. )}
  2234. {/* Add Lot Confirmation Modal */}
  2235. {lotConfirmationOpen && expectedLotData && scannedLotData && (
  2236. <LotConfirmationModal
  2237. open={lotConfirmationOpen}
  2238. onClose={() => {
  2239. console.log(`⏱️ [LOT CONFIRM MODAL] Closing modal, clearing state`);
  2240. setLotConfirmationOpen(false);
  2241. setExpectedLotData(null);
  2242. setScannedLotData(null);
  2243. setSelectedLotForQr(null);
  2244. // ✅ IMPORTANT: Clear refs and processedQrCombinations to allow reprocessing the same QR code
  2245. // This allows the modal to reopen if user cancels and scans the same QR again
  2246. setTimeout(() => {
  2247. lastProcessedQrRef.current = '';
  2248. processedQrCodesRef.current.clear();
  2249. // Clear processedQrCombinations for this itemId+stockInLineId combination
  2250. if (scannedLotData?.stockInLineId && selectedLotForQr?.itemId) {
  2251. setProcessedQrCombinations(prev => {
  2252. const newMap = new Map(prev);
  2253. const itemId = selectedLotForQr.itemId;
  2254. if (itemId && newMap.has(itemId)) {
  2255. newMap.get(itemId)!.delete(scannedLotData.stockInLineId);
  2256. if (newMap.get(itemId)!.size === 0) {
  2257. newMap.delete(itemId);
  2258. }
  2259. }
  2260. return newMap;
  2261. });
  2262. }
  2263. console.log(`⏱️ [LOT CONFIRM MODAL] Cleared refs and processedQrCombinations to allow reprocessing`);
  2264. }, 100);
  2265. }}
  2266. onConfirm={handleLotConfirmation}
  2267. expectedLot={expectedLotData}
  2268. scannedLot={scannedLotData}
  2269. isLoading={isConfirmingLot}
  2270. />
  2271. )}
  2272. {/* Manual Lot Confirmation Modal (test shortcut {2fic}) */}
  2273. <ManualLotConfirmationModal
  2274. open={manualLotConfirmationOpen}
  2275. onClose={() => setManualLotConfirmationOpen(false)}
  2276. // Reuse existing handler: expectedLotInput=current lot, scannedLotInput=new lot
  2277. onConfirm={(currentLotNo, newLotNo) => {
  2278. // Use existing manual flow from handleManualLotConfirmation in other screens:
  2279. // Here we route through updateStockOutLineStatusByQRCodeAndLotNo via handleManualLotConfirmation-like inline logic.
  2280. // For now: open LotConfirmationModal path by setting expected/scanned and letting user confirm substitution.
  2281. setExpectedLotData({ lotNo: currentLotNo, itemCode: '', itemName: '' });
  2282. setScannedLotData({ lotNo: newLotNo, itemCode: '', itemName: '', inventoryLotLineId: null, stockInLineId: null });
  2283. setManualLotConfirmationOpen(false);
  2284. setLotConfirmationOpen(true);
  2285. }}
  2286. expectedLot={expectedLotData}
  2287. scannedLot={scannedLotData}
  2288. isLoading={isConfirmingLot}
  2289. />
  2290. {/* Pick Execution Form Modal */}
  2291. {pickExecutionFormOpen && selectedLotForExecutionForm && (
  2292. <GoodPickExecutionForm
  2293. open={pickExecutionFormOpen}
  2294. onClose={() => {
  2295. setPickExecutionFormOpen(false);
  2296. setSelectedLotForExecutionForm(null);
  2297. }}
  2298. onSubmit={handlePickExecutionFormSubmit}
  2299. selectedLot={selectedLotForExecutionForm}
  2300. selectedPickOrderLine={{
  2301. id: selectedLotForExecutionForm.pickOrderLineId,
  2302. itemId: selectedLotForExecutionForm.itemId,
  2303. itemCode: selectedLotForExecutionForm.itemCode,
  2304. itemName: selectedLotForExecutionForm.itemName,
  2305. pickOrderCode: selectedLotForExecutionForm.pickOrderCode,
  2306. // Add missing required properties from GetPickOrderLineInfo interface
  2307. availableQty: selectedLotForExecutionForm.availableQty || 0,
  2308. requiredQty: selectedLotForExecutionForm.requiredQty || 0,
  2309. uomDesc: selectedLotForExecutionForm.uomDesc || '',
  2310. uomShortDesc: selectedLotForExecutionForm.uomShortDesc || '',
  2311. pickedQty: selectedLotForExecutionForm.actualPickQty || 0,
  2312. suggestedList: [],
  2313. noLotLines: []
  2314. }}
  2315. pickOrderId={selectedLotForExecutionForm.pickOrderId}
  2316. pickOrderCreateDate={new Date()}
  2317. onNormalPickSubmit={async (lot, submitQty) => {
  2318. console.log('onNormalPickSubmit called in newJobPickExecution:', { lot, submitQty });
  2319. if (!lot) {
  2320. console.error('Lot is null or undefined');
  2321. return;
  2322. }
  2323. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  2324. handlePickQtyChange(lotKey, submitQty);
  2325. await handleSubmitPickQtyWithQty(lot, submitQty);
  2326. }}
  2327. />
  2328. )}
  2329. </FormProvider>
  2330. </TestQrCodeProvider>
  2331. );
  2332. };
  2333. export default JobPickExecution