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

newJobPickExecution.tsx 111 KiB

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