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

JobPickExecution.tsx 104 KiB

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