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

GoodPickExecutiondetail.tsx 139 KiB

6ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
5ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
2ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
3週間前
6ヶ月前
4ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
3週間前
2週間前
6ヶ月前
6ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
2ヶ月前
2ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
1ヶ月前
5ヶ月前
1ヶ月前
5ヶ月前
1ヶ月前
5ヶ月前
1ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
2ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
4ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
2ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
2ヶ月前
2ヶ月前
4ヶ月前
4ヶ月前
2ヶ月前
2ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
4ヶ月前
2ヶ月前
2ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
2ヶ月前
4ヶ月前
6ヶ月前
2ヶ月前
2ヶ月前
6ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
2ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
2ヶ月前
3週間前
2ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
3週間前
6ヶ月前
4ヶ月前
6ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
3週間前
6ヶ月前
5ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
6ヶ月前
2週間前
6ヶ月前
2週間前
5ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
6ヶ月前
2週間前
6ヶ月前
2週間前
6ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
6ヶ月前
2ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
2週間前
5ヶ月前
5ヶ月前
2週間前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
2週間前
5ヶ月前
2週間前
4ヶ月前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
3週間前
5ヶ月前
3週間前
3週間前
3週間前
3週間前
5ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
6ヶ月前
3週間前
6ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
3週間前
5ヶ月前
3週間前
5ヶ月前
5ヶ月前
5ヶ月前
3週間前
5ヶ月前
6ヶ月前
3週間前
6ヶ月前
5ヶ月前
6ヶ月前
6ヶ月前
6ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
5ヶ月前
3週間前
5ヶ月前
6ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
4ヶ月前
4ヶ月前
4ヶ月前
2週間前
4ヶ月前
2週間前
4ヶ月前
2週間前
4ヶ月前
2ヶ月前
4ヶ月前
2週間前
2ヶ月前
4ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
5ヶ月前
2ヶ月前
5ヶ月前
5ヶ月前
4ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
5ヶ月前
6ヶ月前
5ヶ月前
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632
  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. Chip,
  21. } from "@mui/material";
  22. import dayjs from 'dayjs';
  23. import TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider';
  24. import { fetchLotDetail } from "@/app/api/inventory/actions";
  25. import React, { useCallback, useEffect, useState, useRef, useMemo, startTransition } from "react";
  26. import { useTranslation } from "react-i18next";
  27. import { useRouter } from "next/navigation";
  28. import {
  29. updateStockOutLineStatus,
  30. createStockOutLine,
  31. updateStockOutLine,
  32. recordPickExecutionIssue,
  33. fetchFGPickOrders, // Add this import
  34. FGPickOrderResponse,
  35. stockReponse,
  36. PickExecutionIssueData,
  37. checkPickOrderCompletion,
  38. fetchAllPickOrderLotsHierarchical,
  39. PickOrderCompletionResponse,
  40. checkAndCompletePickOrderByConsoCode,
  41. updateSuggestedLotLineId,
  42. updateStockOutLineStatusByQRCodeAndLotNo,
  43. confirmLotSubstitution,
  44. fetchDoPickOrderDetail, // 必须添加
  45. DoPickOrderDetail, // 必须添加
  46. fetchFGPickOrdersByUserId ,
  47. batchQrSubmit,
  48. batchSubmitList, // 添加:导入 batchSubmitList
  49. batchSubmitListRequest, // 添加:导入类型
  50. batchSubmitListLineRequest,
  51. batchScan,
  52. BatchScanRequest,
  53. BatchScanLineRequest,
  54. } from "@/app/api/pickOrder/actions";
  55. import FGPickOrderInfoCard from "./FGPickOrderInfoCard";
  56. import LotConfirmationModal from "./LotConfirmationModal";
  57. //import { fetchItem } from "@/app/api/settings/item";
  58. import { updateInventoryLotLineStatus, analyzeQrCode } from "@/app/api/inventory/actions";
  59. import { fetchNameList, NameList } from "@/app/api/user/actions";
  60. import {
  61. FormProvider,
  62. useForm,
  63. } from "react-hook-form";
  64. import SearchBox, { Criterion } from "../SearchBox";
  65. import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
  66. import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
  67. import QrCodeIcon from '@mui/icons-material/QrCode';
  68. import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
  69. import { useSession } from "next-auth/react";
  70. import { SessionWithTokens } from "@/config/authConfig";
  71. import { fetchStockInLineInfo } from "@/app/api/po/actions";
  72. import GoodPickExecutionForm from "./GoodPickExecutionForm";
  73. import FGPickOrderCard from "./FGPickOrderCard";
  74. import LinearProgressWithLabel from "../common/LinearProgressWithLabel";
  75. import ScanStatusAlert from "../common/ScanStatusAlert";
  76. interface Props {
  77. filterArgs: Record<string, any>;
  78. onSwitchToRecordTab?: () => void;
  79. onRefreshReleasedOrderCount?: () => void;
  80. }
  81. // QR Code Modal Component (from LotTable)
  82. const QrCodeModal: React.FC<{
  83. open: boolean;
  84. onClose: () => void;
  85. lot: any | null;
  86. onQrCodeSubmit: (lotNo: string) => void;
  87. combinedLotData: any[]; // Add this prop
  88. lotConfirmationOpen: boolean;
  89. }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData,lotConfirmationOpen = false }) => {
  90. const { t } = useTranslation("pickOrder");
  91. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  92. const [manualInput, setManualInput] = useState<string>('');
  93. const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false);
  94. const [manualInputError, setManualInputError] = useState<boolean>(false);
  95. const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false);
  96. const [qrScanFailed, setQrScanFailed] = useState<boolean>(false);
  97. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  98. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  99. const [scannedQrResult, setScannedQrResult] = useState<string>('');
  100. const [fgPickOrder, setFgPickOrder] = useState<FGPickOrderResponse | null>(null);
  101. const fetchingRef = useRef<Set<number>>(new Set());
  102. // Process scanned QR codes
  103. useEffect(() => {
  104. // ✅ Don't process if modal is not open
  105. if (!open) {
  106. return;
  107. }
  108. // ✅ Don't process if lot confirmation modal is open
  109. if (lotConfirmationOpen) {
  110. console.log("Lot confirmation modal is open, skipping QrCodeModal processing...");
  111. return;
  112. }
  113. if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) {
  114. const latestQr = qrValues[qrValues.length - 1];
  115. if (processedQrCodes.has(latestQr)) {
  116. console.log("QR code already processed, skipping...");
  117. return;
  118. }
  119. try {
  120. const qrData = JSON.parse(latestQr);
  121. if (qrData.stockInLineId && qrData.itemId) {
  122. // ✅ Check if we're already fetching this stockInLineId
  123. if (fetchingRef.current.has(qrData.stockInLineId)) {
  124. console.log(`⏱️ [QR MODAL] Already fetching stockInLineId: ${qrData.stockInLineId}, skipping duplicate call`);
  125. return;
  126. }
  127. setProcessedQrCodes(prev => new Set(prev).add(latestQr));
  128. setIsProcessingQr(true);
  129. setQrScanFailed(false);
  130. // ✅ Mark as fetching
  131. fetchingRef.current.add(qrData.stockInLineId);
  132. const fetchStartTime = performance.now();
  133. console.log(`⏱️ [QR MODAL] Starting fetchStockInLineInfo for stockInLineId: ${qrData.stockInLineId}`);
  134. fetchStockInLineInfo(qrData.stockInLineId)
  135. .then((stockInLineInfo) => {
  136. // ✅ Remove from fetching set
  137. fetchingRef.current.delete(qrData.stockInLineId);
  138. // ✅ Check again if modal is still open and lot confirmation is not open
  139. if (!open || lotConfirmationOpen) {
  140. console.log("Modal state changed, skipping result processing");
  141. return;
  142. }
  143. const fetchTime = performance.now() - fetchStartTime;
  144. console.log(`⏱️ [QR MODAL] fetchStockInLineInfo time: ${fetchTime.toFixed(2)}ms (${(fetchTime / 1000).toFixed(3)}s)`);
  145. console.log("Stock in line info:", stockInLineInfo);
  146. setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
  147. if (stockInLineInfo.lotNo === lot.lotNo) {
  148. console.log(` QR Code verified for lot: ${lot.lotNo}`);
  149. setQrScanSuccess(true);
  150. onQrCodeSubmit(lot.lotNo);
  151. // onClose();
  152. //resetScan();
  153. } else {
  154. console.log(` QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`);
  155. setQrScanFailed(true);
  156. setManualInputError(true);
  157. setManualInputSubmitted(true);
  158. }
  159. })
  160. .catch((error) => {
  161. // ✅ Remove from fetching set
  162. fetchingRef.current.delete(qrData.stockInLineId);
  163. // ✅ Check again if modal is still open
  164. if (!open || lotConfirmationOpen) {
  165. console.log("Modal state changed, skipping error handling");
  166. return;
  167. }
  168. const fetchTime = performance.now() - fetchStartTime;
  169. console.error(`❌ [QR MODAL] fetchStockInLineInfo failed after ${fetchTime.toFixed(2)}ms:`, error);
  170. setScannedQrResult('Error fetching data');
  171. setQrScanFailed(true);
  172. setManualInputError(true);
  173. setManualInputSubmitted(true);
  174. })
  175. .finally(() => {
  176. setIsProcessingQr(false);
  177. });
  178. } else {
  179. const qrContent = latestQr.replace(/[{}]/g, '');
  180. setScannedQrResult(qrContent);
  181. if (qrContent === lot.lotNo) {
  182. setQrScanSuccess(true);
  183. onQrCodeSubmit(lot.lotNo);
  184. onClose();
  185. resetScan();
  186. } else {
  187. setQrScanFailed(true);
  188. setManualInputError(true);
  189. setManualInputSubmitted(true);
  190. }
  191. }
  192. } catch (error) {
  193. console.log("QR code is not JSON format, trying direct comparison");
  194. const qrContent = latestQr.replace(/[{}]/g, '');
  195. setScannedQrResult(qrContent);
  196. if (qrContent === lot.lotNo) {
  197. setQrScanSuccess(true);
  198. onQrCodeSubmit(lot.lotNo);
  199. onClose();
  200. resetScan();
  201. } else {
  202. setQrScanFailed(true);
  203. setManualInputError(true);
  204. setManualInputSubmitted(true);
  205. }
  206. }
  207. }
  208. }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes, lotConfirmationOpen, open]);
  209. // Clear states when modal opens
  210. useEffect(() => {
  211. if (open) {
  212. setManualInput('');
  213. setManualInputSubmitted(false);
  214. setManualInputError(false);
  215. setIsProcessingQr(false);
  216. setQrScanFailed(false);
  217. setQrScanSuccess(false);
  218. setScannedQrResult('');
  219. setProcessedQrCodes(new Set());
  220. }
  221. }, [open]);
  222. useEffect(() => {
  223. if (lot) {
  224. setManualInput('');
  225. setManualInputSubmitted(false);
  226. setManualInputError(false);
  227. setIsProcessingQr(false);
  228. setQrScanFailed(false);
  229. setQrScanSuccess(false);
  230. setScannedQrResult('');
  231. setProcessedQrCodes(new Set());
  232. }
  233. }, [lot]);
  234. // Auto-submit manual input when it matches
  235. useEffect(() => {
  236. if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) {
  237. console.log(' Auto-submitting manual input:', manualInput.trim());
  238. const timer = setTimeout(() => {
  239. setQrScanSuccess(true);
  240. onQrCodeSubmit(lot.lotNo);
  241. onClose();
  242. setManualInput('');
  243. setManualInputError(false);
  244. setManualInputSubmitted(false);
  245. }, 200);
  246. return () => clearTimeout(timer);
  247. }
  248. }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]);
  249. const handleManualSubmit = () => {
  250. if (manualInput.trim() === lot?.lotNo) {
  251. setQrScanSuccess(true);
  252. onQrCodeSubmit(lot.lotNo);
  253. onClose();
  254. setManualInput('');
  255. } else {
  256. setQrScanFailed(true);
  257. setManualInputError(true);
  258. setManualInputSubmitted(true);
  259. }
  260. };
  261. useEffect(() => {
  262. if (open) {
  263. startScan();
  264. }
  265. }, [open, startScan]);
  266. return (
  267. <Modal open={open} onClose={onClose}>
  268. <Box sx={{
  269. position: 'absolute',
  270. top: '50%',
  271. left: '50%',
  272. transform: 'translate(-50%, -50%)',
  273. bgcolor: 'background.paper',
  274. p: 3,
  275. borderRadius: 2,
  276. minWidth: 400,
  277. }}>
  278. <Typography variant="h6" gutterBottom>
  279. {t("QR Code Scan for Lot")}: {lot?.lotNo}
  280. </Typography>
  281. {isProcessingQr && (
  282. <Box sx={{ mb: 2, p: 2, backgroundColor: '#e3f2fd', borderRadius: 1 }}>
  283. <Typography variant="body2" color="primary">
  284. {t("Processing QR code...")}
  285. </Typography>
  286. </Box>
  287. )}
  288. <Box sx={{ mb: 2 }}>
  289. <Typography variant="body2" gutterBottom>
  290. <strong>{t("Manual Input")}:</strong>
  291. </Typography>
  292. <TextField
  293. fullWidth
  294. size="small"
  295. value={manualInput}
  296. onChange={(e) => {
  297. setManualInput(e.target.value);
  298. if (qrScanFailed || manualInputError) {
  299. setQrScanFailed(false);
  300. setManualInputError(false);
  301. setManualInputSubmitted(false);
  302. }
  303. }}
  304. sx={{ mb: 1 }}
  305. error={manualInputSubmitted && manualInputError}
  306. helperText={
  307. manualInputSubmitted && manualInputError
  308. ? `${t("The input is not the same as the expected lot number.")}`
  309. : ''
  310. }
  311. />
  312. <Button
  313. variant="contained"
  314. onClick={handleManualSubmit}
  315. disabled={!manualInput.trim()}
  316. size="small"
  317. color="primary"
  318. >
  319. {t("Submit")}
  320. </Button>
  321. </Box>
  322. {qrValues.length > 0 && (
  323. <Box sx={{
  324. mb: 2,
  325. p: 2,
  326. backgroundColor: qrScanFailed ? '#ffebee' : qrScanSuccess ? '#e8f5e8' : '#f5f5f5',
  327. borderRadius: 1
  328. }}>
  329. <Typography variant="body2" color={qrScanFailed ? 'error' : qrScanSuccess ? 'success' : 'text.secondary'}>
  330. <strong>{t("QR Scan Result:")}</strong> {scannedQrResult}
  331. </Typography>
  332. {qrScanSuccess && (
  333. <Typography variant="caption" color="success" display="block">
  334. {t("Verified successfully!")}
  335. </Typography>
  336. )}
  337. </Box>
  338. )}
  339. <Box sx={{ mt: 2, textAlign: 'right' }}>
  340. <Button onClick={onClose} variant="outlined">
  341. {t("Cancel")}
  342. </Button>
  343. </Box>
  344. </Box>
  345. </Modal>
  346. );
  347. };
  348. const ManualLotConfirmationModal: React.FC<{
  349. open: boolean;
  350. onClose: () => void;
  351. onConfirm: (expectedLotNo: string, scannedLotNo: string) => void;
  352. expectedLot: {
  353. lotNo: string;
  354. itemCode: string;
  355. itemName: string;
  356. } | null;
  357. scannedLot: {
  358. lotNo: string;
  359. itemCode: string;
  360. itemName: string;
  361. } | null;
  362. isLoading?: boolean;
  363. }> = ({ open, onClose, onConfirm, expectedLot, scannedLot, isLoading = false }) => {
  364. const { t } = useTranslation("pickOrder");
  365. const [expectedLotInput, setExpectedLotInput] = useState<string>('');
  366. const [scannedLotInput, setScannedLotInput] = useState<string>('');
  367. const [error, setError] = useState<string>('');
  368. // 当模态框打开时,预填充输入框
  369. useEffect(() => {
  370. if (open) {
  371. setExpectedLotInput(expectedLot?.lotNo || '');
  372. setScannedLotInput(scannedLot?.lotNo || '');
  373. setError('');
  374. }
  375. }, [open, expectedLot, scannedLot]);
  376. const handleConfirm = () => {
  377. if (!expectedLotInput.trim() || !scannedLotInput.trim()) {
  378. setError(t("Please enter both expected and scanned lot numbers."));
  379. return;
  380. }
  381. if (expectedLotInput.trim() === scannedLotInput.trim()) {
  382. setError(t("Expected and scanned lot numbers cannot be the same."));
  383. return;
  384. }
  385. onConfirm(expectedLotInput.trim(), scannedLotInput.trim());
  386. };
  387. return (
  388. <Modal open={open} onClose={onClose}>
  389. <Box sx={{
  390. position: 'absolute',
  391. top: '50%',
  392. left: '50%',
  393. transform: 'translate(-50%, -50%)',
  394. bgcolor: 'background.paper',
  395. p: 3,
  396. borderRadius: 2,
  397. minWidth: 500,
  398. }}>
  399. <Typography variant="h6" gutterBottom color="warning.main">
  400. {t("Manual Lot Confirmation")}
  401. </Typography>
  402. <Box sx={{ mb: 2 }}>
  403. <Typography variant="body2" gutterBottom>
  404. <strong>{t("Expected Lot Number")}:</strong>
  405. </Typography>
  406. <TextField
  407. fullWidth
  408. size="small"
  409. value={expectedLotInput}
  410. onChange={(e) => {
  411. setExpectedLotInput(e.target.value);
  412. setError('');
  413. }}
  414. placeholder={expectedLot?.lotNo || t("Enter expected lot number")}
  415. sx={{ mb: 2 }}
  416. error={!!error && !expectedLotInput.trim()}
  417. />
  418. </Box>
  419. <Box sx={{ mb: 2 }}>
  420. <Typography variant="body2" gutterBottom>
  421. <strong>{t("Scanned Lot Number")}:</strong>
  422. </Typography>
  423. <TextField
  424. fullWidth
  425. size="small"
  426. value={scannedLotInput}
  427. onChange={(e) => {
  428. setScannedLotInput(e.target.value);
  429. setError('');
  430. }}
  431. placeholder={scannedLot?.lotNo || t("Enter scanned lot number")}
  432. sx={{ mb: 2 }}
  433. error={!!error && !scannedLotInput.trim()}
  434. />
  435. </Box>
  436. {error && (
  437. <Box sx={{ mb: 2, p: 1, backgroundColor: '#ffebee', borderRadius: 1 }}>
  438. <Typography variant="body2" color="error">
  439. {error}
  440. </Typography>
  441. </Box>
  442. )}
  443. <Box sx={{ mt: 2, display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
  444. <Button onClick={onClose} variant="outlined" disabled={isLoading}>
  445. {t("Cancel")}
  446. </Button>
  447. <Button
  448. onClick={handleConfirm}
  449. variant="contained"
  450. color="warning"
  451. disabled={isLoading || !expectedLotInput.trim() || !scannedLotInput.trim()}
  452. >
  453. {isLoading ? t("Processing...") : t("Confirm")}
  454. </Button>
  455. </Box>
  456. </Box>
  457. </Modal>
  458. );
  459. };
  460. const PickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab, onRefreshReleasedOrderCount }) => {
  461. const { t } = useTranslation("pickOrder");
  462. const router = useRouter();
  463. const { data: session } = useSession() as { data: SessionWithTokens | null };
  464. const [doPickOrderDetail, setDoPickOrderDetail] = useState<DoPickOrderDetail | null>(null);
  465. const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | null>(null);
  466. const [pickOrderSwitching, setPickOrderSwitching] = useState(false);
  467. const currentUserId = session?.id ? parseInt(session.id) : undefined;
  468. const [allLotsCompleted, setAllLotsCompleted] = useState(false);
  469. const [combinedLotData, setCombinedLotData] = useState<any[]>([]);
  470. const [combinedDataLoading, setCombinedDataLoading] = useState(false);
  471. const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]);
  472. // issue form 里填的 actualPickQty(用于 batch submit 只提交实际拣到数量,而不是补拣到 required)
  473. const [issuePickedQtyBySolId, setIssuePickedQtyBySolId] = useState<Record<number, number>>({});
  474. // 防止重复点击(Submit / Just Completed / Issue)
  475. const [actionBusyBySolId, setActionBusyBySolId] = useState<Record<number, boolean>>({});
  476. const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
  477. const [qrScanInput, setQrScanInput] = useState<string>('');
  478. const [qrScanError, setQrScanError] = useState<boolean>(false);
  479. const [qrScanErrorMsg, setQrScanErrorMsg] = useState<string>('');
  480. const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
  481. const [manualLotConfirmationOpen, setManualLotConfirmationOpen] = useState(false);
  482. const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({});
  483. const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
  484. const [paginationController, setPaginationController] = useState({
  485. pageNum: 0,
  486. pageSize: -1,
  487. });
  488. const [usernameList, setUsernameList] = useState<NameList[]>([]);
  489. const initializationRef = useRef(false);
  490. const autoAssignRef = useRef(false);
  491. const formProps = useForm();
  492. const errors = formProps.formState.errors;
  493. // QR scanner states (always-on, no modal)
  494. const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);
  495. const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false);
  496. const [expectedLotData, setExpectedLotData] = useState<any>(null);
  497. const [scannedLotData, setScannedLotData] = useState<any>(null);
  498. const [isConfirmingLot, setIsConfirmingLot] = useState(false);
  499. // Add GoodPickExecutionForm states
  500. const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
  501. const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
  502. const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);
  503. const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
  504. // Add these missing state variables after line 352
  505. const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
  506. // Track processed QR codes by itemId+stockInLineId combination for better lot confirmation handling
  507. const [processedQrCombinations, setProcessedQrCombinations] = useState<Map<number, Set<number>>>(new Map());
  508. const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
  509. const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
  510. const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
  511. const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false);
  512. // Cache for fetchStockInLineInfo API calls to avoid redundant requests
  513. const stockInLineInfoCache = useRef<Map<number, { lotNo: string | null; timestamp: number }>>(new Map());
  514. const CACHE_TTL = 60000; // 60 seconds cache TTL
  515. const abortControllerRef = useRef<AbortController | null>(null);
  516. const qrProcessingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
  517. // Use refs for processed QR tracking to avoid useEffect dependency issues and delays
  518. const processedQrCodesRef = useRef<Set<string>>(new Set());
  519. const lastProcessedQrRef = useRef<string>('');
  520. // Store callbacks in refs to avoid useEffect dependency issues
  521. const processOutsideQrCodeRef = useRef<((latestQr: string) => Promise<void>) | null>(null);
  522. const resetScanRef = useRef<(() => void) | null>(null);
  523. const lotConfirmOpenedQrCountRef = useRef<number>(0);
  524. const lotConfirmOpenedQrValueRef = useRef<string>('');
  525. const lotConfirmInitialSameQrSkippedRef = useRef<boolean>(false);
  526. const autoConfirmInProgressRef = useRef<boolean>(false);
  527. // Handle QR code button click
  528. const handleQrCodeClick = (pickOrderId: number) => {
  529. console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
  530. // TODO: Implement QR code functionality
  531. };
  532. const progress = useMemo(() => {
  533. if (combinedLotData.length === 0) {
  534. return { completed: 0, total: 0 };
  535. }
  536. const nonPendingCount = combinedLotData.filter(lot => {
  537. const status = lot.stockOutLineStatus?.toLowerCase();
  538. return status !== 'pending';
  539. }).length;
  540. return {
  541. completed: nonPendingCount,
  542. total: combinedLotData.length
  543. };
  544. }, [combinedLotData]);
  545. // Cached version of fetchStockInLineInfo to avoid redundant API calls
  546. const fetchStockInLineInfoCached = useCallback(async (stockInLineId: number): Promise<{ lotNo: string | null }> => {
  547. const now = Date.now();
  548. const cached = stockInLineInfoCache.current.get(stockInLineId);
  549. // Return cached value if still valid
  550. if (cached && (now - cached.timestamp) < CACHE_TTL) {
  551. console.log(`✅ [CACHE HIT] Using cached stockInLineInfo for ${stockInLineId}`);
  552. return { lotNo: cached.lotNo };
  553. }
  554. // Cancel previous request if exists
  555. if (abortControllerRef.current) {
  556. abortControllerRef.current.abort();
  557. }
  558. // Create new abort controller for this request
  559. const abortController = new AbortController();
  560. abortControllerRef.current = abortController;
  561. try {
  562. console.log(`⏱️ [CACHE MISS] Fetching stockInLineInfo for ${stockInLineId}`);
  563. const stockInLineInfo = await fetchStockInLineInfo(stockInLineId);
  564. // Store in cache
  565. stockInLineInfoCache.current.set(stockInLineId, {
  566. lotNo: stockInLineInfo.lotNo || null,
  567. timestamp: now
  568. });
  569. // Limit cache size to prevent memory leaks
  570. if (stockInLineInfoCache.current.size > 100) {
  571. const firstKey = stockInLineInfoCache.current.keys().next().value;
  572. if (firstKey !== undefined) {
  573. stockInLineInfoCache.current.delete(firstKey);
  574. }
  575. }
  576. return { lotNo: stockInLineInfo.lotNo || null };
  577. } catch (error: any) {
  578. if (error.name === 'AbortError') {
  579. console.log(`⏱️ [CACHE] Request aborted for ${stockInLineId}`);
  580. throw error;
  581. }
  582. console.error(`❌ [CACHE] Error fetching stockInLineInfo for ${stockInLineId}:`, error);
  583. throw error;
  584. }
  585. }, []);
  586. const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => {
  587. const mismatchStartTime = performance.now();
  588. console.log(`⏱️ [HANDLE LOT MISMATCH START]`);
  589. console.log(`⏰ Start time: ${new Date().toISOString()}`);
  590. console.log("Lot mismatch detected:", { expectedLot, scannedLot });
  591. // ✅ Use setTimeout to avoid flushSync warning - schedule modal update in next tick
  592. const setTimeoutStartTime = performance.now();
  593. console.time('setLotConfirmationOpen');
  594. setTimeout(() => {
  595. const setStateStartTime = performance.now();
  596. setExpectedLotData(expectedLot);
  597. setScannedLotData({
  598. ...scannedLot,
  599. lotNo: scannedLot.lotNo || null,
  600. });
  601. setLotConfirmationOpen(true);
  602. const setStateTime = performance.now() - setStateStartTime;
  603. console.timeEnd('setLotConfirmationOpen');
  604. console.log(`⏱️ [HANDLE LOT MISMATCH] Modal state set to open (setState time: ${setStateTime.toFixed(2)}ms)`);
  605. console.log(`✅ [HANDLE LOT MISMATCH] Modal state set to open`);
  606. }, 0);
  607. const setTimeoutTime = performance.now() - setTimeoutStartTime;
  608. console.log(`⏱️ [PERF] setTimeout scheduling time: ${setTimeoutTime.toFixed(2)}ms`);
  609. // ✅ Fetch lotNo in background ONLY for display purposes (using cached version)
  610. if (!scannedLot.lotNo && scannedLot.stockInLineId) {
  611. const stockInLineId = scannedLot.stockInLineId;
  612. if (typeof stockInLineId !== 'number') {
  613. console.warn(`⏱️ [HANDLE LOT MISMATCH] Invalid stockInLineId: ${stockInLineId}`);
  614. return;
  615. }
  616. console.log(`⏱️ [HANDLE LOT MISMATCH] Fetching lotNo in background (stockInLineId: ${stockInLineId})`);
  617. const fetchStartTime = performance.now();
  618. fetchStockInLineInfoCached(stockInLineId)
  619. .then((stockInLineInfo) => {
  620. const fetchTime = performance.now() - fetchStartTime;
  621. console.log(`⏱️ [HANDLE LOT MISMATCH] fetchStockInLineInfoCached time: ${fetchTime.toFixed(2)}ms (${(fetchTime / 1000).toFixed(3)}s)`);
  622. const updateStateStartTime = performance.now();
  623. startTransition(() => {
  624. setScannedLotData((prev: any) => ({
  625. ...prev,
  626. lotNo: stockInLineInfo.lotNo || null,
  627. }));
  628. });
  629. const updateStateTime = performance.now() - updateStateStartTime;
  630. console.log(`⏱️ [PERF] Update scanned lot data time: ${updateStateTime.toFixed(2)}ms`);
  631. const totalTime = performance.now() - mismatchStartTime;
  632. console.log(`⏱️ [HANDLE LOT MISMATCH] Background fetch completed: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  633. })
  634. .catch((error) => {
  635. if (error.name !== 'AbortError') {
  636. const fetchTime = performance.now() - fetchStartTime;
  637. console.error(`❌ [HANDLE LOT MISMATCH] fetchStockInLineInfoCached failed after ${fetchTime.toFixed(2)}ms:`, error);
  638. }
  639. });
  640. } else {
  641. const totalTime = performance.now() - mismatchStartTime;
  642. console.log(`⏱️ [HANDLE LOT MISMATCH END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  643. }
  644. }, [fetchStockInLineInfoCached]);
  645. const checkAllLotsCompleted = useCallback((lotData: any[]) => {
  646. if (lotData.length === 0) {
  647. setAllLotsCompleted(false);
  648. return false;
  649. }
  650. // Filter out rejected lots
  651. const nonRejectedLots = lotData.filter(lot =>
  652. lot.lotAvailability !== 'rejected' &&
  653. lot.stockOutLineStatus !== 'rejected'
  654. );
  655. if (nonRejectedLots.length === 0) {
  656. setAllLotsCompleted(false);
  657. return false;
  658. }
  659. // Check if all non-rejected lots are completed
  660. const allCompleted = nonRejectedLots.every(lot =>
  661. lot.stockOutLineStatus === 'completed'
  662. );
  663. setAllLotsCompleted(allCompleted);
  664. return allCompleted;
  665. }, []);
  666. // 在 fetchAllCombinedLotData 函数中(约 446-684 行)
  667. const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdOverride?: number) => {
  668. setCombinedDataLoading(true);
  669. try {
  670. const userIdToUse = userId || currentUserId;
  671. console.log(" fetchAllCombinedLotData called with userId:", userIdToUse);
  672. if (!userIdToUse) {
  673. console.warn("⚠️ No userId available, skipping API call");
  674. setCombinedLotData([]);
  675. setOriginalCombinedData([]);
  676. setAllLotsCompleted(false);
  677. return;
  678. }
  679. // 获取新结构的层级数据
  680. const hierarchicalData = await fetchAllPickOrderLotsHierarchical(userIdToUse);
  681. console.log(" Hierarchical data (new structure):", hierarchicalData);
  682. // 检查数据结构
  683. if (!hierarchicalData.fgInfo || !hierarchicalData.pickOrders || hierarchicalData.pickOrders.length === 0) {
  684. console.warn("⚠️ No FG info or pick orders found");
  685. setCombinedLotData([]);
  686. setOriginalCombinedData([]);
  687. setAllLotsCompleted(false);
  688. return;
  689. }
  690. // 使用合并后的 pick order 对象(现在只有一个对象,包含所有数据)
  691. const mergedPickOrder = hierarchicalData.pickOrders[0];
  692. // 设置 FG info 到 fgPickOrders(用于显示 FG 信息卡片)
  693. // 修改第 478-509 行的 fgOrder 构建逻辑:
  694. const fgOrder: FGPickOrderResponse = {
  695. doPickOrderId: hierarchicalData.fgInfo.doPickOrderId,
  696. ticketNo: hierarchicalData.fgInfo.ticketNo,
  697. storeId: hierarchicalData.fgInfo.storeId,
  698. shopCode: hierarchicalData.fgInfo.shopCode,
  699. shopName: hierarchicalData.fgInfo.shopName,
  700. truckLanceCode: hierarchicalData.fgInfo.truckLanceCode,
  701. DepartureTime: hierarchicalData.fgInfo.departureTime,
  702. shopAddress: "",
  703. pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
  704. // 兼容字段(注意 consoCodes 是数组)
  705. pickOrderId: mergedPickOrder.pickOrderIds?.[0] || 0,
  706. pickOrderConsoCode: Array.isArray(mergedPickOrder.consoCodes)
  707. ? mergedPickOrder.consoCodes[0] || ""
  708. : "",
  709. pickOrderTargetDate: mergedPickOrder.targetDate || "",
  710. pickOrderStatus: mergedPickOrder.status || "",
  711. deliveryOrderId: mergedPickOrder.doOrderIds?.[0] || 0,
  712. deliveryNo: mergedPickOrder.deliveryOrderCodes?.[0] || "",
  713. deliveryDate: "",
  714. shopId: 0,
  715. shopPoNo: "",
  716. numberOfCartons: mergedPickOrder.pickOrderLines?.length || 0,
  717. qrCodeData: hierarchicalData.fgInfo.doPickOrderId,
  718. // 多个 pick orders 信息:全部保留为数组
  719. numberOfPickOrders: mergedPickOrder.pickOrderIds?.length || 0,
  720. pickOrderIds: mergedPickOrder.pickOrderIds || [],
  721. pickOrderCodes: Array.isArray(mergedPickOrder.pickOrderCodes)
  722. ? mergedPickOrder.pickOrderCodes
  723. : [],
  724. deliveryOrderIds: mergedPickOrder.doOrderIds || [],
  725. deliveryNos: Array.isArray(mergedPickOrder.deliveryOrderCodes)
  726. ? mergedPickOrder.deliveryOrderCodes
  727. : [],
  728. lineCountsPerPickOrder: Array.isArray(mergedPickOrder.lineCountsPerPickOrder)
  729. ? mergedPickOrder.lineCountsPerPickOrder
  730. : [],
  731. };
  732. setFgPickOrders([fgOrder]);
  733. console.log(" DEBUG fgOrder.lineCountsPerPickOrder:", fgOrder.lineCountsPerPickOrder);
  734. console.log(" DEBUG fgOrder.pickOrderCodes:", fgOrder.pickOrderCodes);
  735. console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
  736. // 直接使用合并后的 pickOrderLines
  737. console.log("🎯 Displaying merged pick order lines");
  738. // 将层级数据转换为平铺格式(用于表格显示)
  739. const flatLotData: any[] = [];
  740. mergedPickOrder.pickOrderLines.forEach((line: any) => {
  741. // 用来记录这一行已经通过 lots 出现过的 lotId
  742. const lotIdSet = new Set<number>();
  743. // ✅ lots:按 lotId 去重并合并 requiredQty
  744. if (line.lots && line.lots.length > 0) {
  745. const lotMap = new Map<number, any>();
  746. line.lots.forEach((lot: any) => {
  747. const lotId = lot.id;
  748. if (lotMap.has(lotId)) {
  749. const existingLot = lotMap.get(lotId);
  750. existingLot.requiredQty =
  751. (existingLot.requiredQty || 0) + (lot.requiredQty || 0);
  752. } else {
  753. lotMap.set(lotId, { ...lot });
  754. }
  755. });
  756. lotMap.forEach((lot: any) => {
  757. if (lot.id != null) {
  758. lotIdSet.add(lot.id);
  759. }
  760. flatLotData.push({
  761. pickOrderConsoCode: Array.isArray(mergedPickOrder.consoCodes)
  762. ? mergedPickOrder.consoCodes[0] || ""
  763. : "",
  764. pickOrderTargetDate: mergedPickOrder.targetDate,
  765. pickOrderStatus: mergedPickOrder.status,
  766. pickOrderId: line.pickOrderId || mergedPickOrder.pickOrderIds?.[0] || 0,
  767. pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
  768. pickOrderLineId: line.id,
  769. pickOrderLineRequiredQty: line.requiredQty,
  770. pickOrderLineStatus: line.status,
  771. itemId: line.item.id,
  772. itemCode: line.item.code,
  773. itemName: line.item.name,
  774. uomDesc: line.item.uomDesc,
  775. uomShortDesc: line.item.uomShortDesc,
  776. lotId: lot.id,
  777. lotNo: lot.lotNo,
  778. expiryDate: lot.expiryDate,
  779. location: lot.location,
  780. stockUnit: lot.stockUnit,
  781. availableQty: lot.availableQty,
  782. requiredQty: lot.requiredQty,
  783. actualPickQty: lot.actualPickQty,
  784. inQty: lot.inQty,
  785. outQty: lot.outQty,
  786. holdQty: lot.holdQty,
  787. lotStatus: lot.lotStatus,
  788. lotAvailability: lot.lotAvailability,
  789. processingStatus: lot.processingStatus,
  790. suggestedPickLotId: lot.suggestedPickLotId,
  791. stockOutLineId: lot.stockOutLineId,
  792. stockOutLineStatus: lot.stockOutLineStatus,
  793. stockOutLineQty: lot.stockOutLineQty,
  794. stockInLineId: lot.stockInLineId,
  795. routerId: lot.router?.id,
  796. routerIndex: lot.router?.index,
  797. routerRoute: lot.router?.route,
  798. routerArea: lot.router?.area,
  799. noLot: false,
  800. });
  801. });
  802. }
  803. // ✅ stockouts:只保留“真正无批次 / 未在 lots 出现过”的行
  804. if (line.stockouts && line.stockouts.length > 0) {
  805. line.stockouts.forEach((stockout: any) => {
  806. const hasLot = stockout.lotId != null;
  807. const lotAlreadyInLots =
  808. hasLot && lotIdSet.has(stockout.lotId as number);
  809. // 有批次 & 已经通过 lots 渲染过 → 跳过,避免一条变两行
  810. if (!stockout.noLot && lotAlreadyInLots) {
  811. return;
  812. }
  813. // 只渲染:
  814. // - noLot === true 的 Null stock 行
  815. // - 或者 lotId 在 lots 中不存在的特殊情况
  816. flatLotData.push({
  817. pickOrderConsoCode: Array.isArray(mergedPickOrder.consoCodes)
  818. ? mergedPickOrder.consoCodes[0] || ""
  819. : "",
  820. pickOrderTargetDate: mergedPickOrder.targetDate,
  821. pickOrderStatus: mergedPickOrder.status,
  822. pickOrderId: line.pickOrderId || mergedPickOrder.pickOrderIds?.[0] || 0,
  823. pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
  824. pickOrderLineId: line.id,
  825. pickOrderLineRequiredQty: line.requiredQty,
  826. pickOrderLineStatus: line.status,
  827. itemId: line.item.id,
  828. itemCode: line.item.code,
  829. itemName: line.item.name,
  830. uomDesc: line.item.uomDesc,
  831. uomShortDesc: line.item.uomShortDesc,
  832. lotId: stockout.lotId || null,
  833. lotNo: stockout.lotNo || null,
  834. expiryDate: null,
  835. location: stockout.location || null,
  836. stockUnit: line.item.uomDesc,
  837. availableQty: stockout.availableQty || 0,
  838. requiredQty: line.requiredQty,
  839. actualPickQty: stockout.qty || 0,
  840. inQty: 0,
  841. outQty: 0,
  842. holdQty: 0,
  843. lotStatus: stockout.noLot ? "unavailable" : "available",
  844. lotAvailability: stockout.noLot ? "insufficient_stock" : "available",
  845. processingStatus: stockout.status || "pending",
  846. suggestedPickLotId: null,
  847. stockOutLineId: stockout.id || null,
  848. stockOutLineStatus: stockout.status || null,
  849. stockOutLineQty: stockout.qty || 0,
  850. routerId: null,
  851. routerIndex: stockout.noLot ? 999999 : null,
  852. routerRoute: null,
  853. routerArea: null,
  854. noLot: !!stockout.noLot,
  855. });
  856. });
  857. }
  858. });
  859. console.log(" Transformed flat lot data:", flatLotData);
  860. console.log(" Total items (including null stock):", flatLotData.length);
  861. setCombinedLotData(flatLotData);
  862. setOriginalCombinedData(flatLotData);
  863. checkAllLotsCompleted(flatLotData);
  864. } catch (error) {
  865. console.error(" Error fetching combined lot data:", error);
  866. setCombinedLotData([]);
  867. setOriginalCombinedData([]);
  868. setAllLotsCompleted(false);
  869. } finally {
  870. setCombinedDataLoading(false);
  871. }
  872. }, [currentUserId, checkAllLotsCompleted]); // 移除 selectedPickOrderId 依赖
  873. // Add effect to check completion when lot data changes
  874. const handleManualLotConfirmation = useCallback(async (currentLotNo: string, newLotNo: string) => {
  875. console.log(` Manual lot confirmation: Current=${currentLotNo}, New=${newLotNo}`);
  876. // 使用第一个输入框的 lot number 查找当前数据
  877. const currentLot = combinedLotData.find(lot =>
  878. lot.lotNo && lot.lotNo === currentLotNo
  879. );
  880. if (!currentLot) {
  881. console.error(`❌ Current lot not found: ${currentLotNo}`);
  882. alert(t("Current lot number not found. Please verify and try again."));
  883. return;
  884. }
  885. if (!currentLot.stockOutLineId) {
  886. console.error("❌ No stockOutLineId found for current lot");
  887. alert(t("No stock out line found for current lot. Please contact administrator."));
  888. return;
  889. }
  890. setIsConfirmingLot(true);
  891. try {
  892. // 调用 updateStockOutLineStatusByQRCodeAndLotNo API
  893. // 第一个 lot 用于获取 pickOrderLineId, stockOutLineId, itemId
  894. // 第二个 lot 作为 inventoryLotNo
  895. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  896. pickOrderLineId: currentLot.pickOrderLineId,
  897. inventoryLotNo: newLotNo, // 第二个输入框的值
  898. stockOutLineId: currentLot.stockOutLineId,
  899. itemId: currentLot.itemId,
  900. status: "checked",
  901. });
  902. console.log("📥 updateStockOutLineStatusByQRCodeAndLotNo result:", res);
  903. if (res.code === "checked" || res.code === "SUCCESS") {
  904. // ✅ 更新本地状态
  905. const entity = res.entity as any;
  906. setCombinedLotData(prev => prev.map(lot => {
  907. if (lot.stockOutLineId === currentLot.stockOutLineId &&
  908. lot.pickOrderLineId === currentLot.pickOrderLineId) {
  909. return {
  910. ...lot,
  911. stockOutLineStatus: 'checked',
  912. stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty,
  913. };
  914. }
  915. return lot;
  916. }));
  917. setOriginalCombinedData(prev => prev.map(lot => {
  918. if (lot.stockOutLineId === currentLot.stockOutLineId &&
  919. lot.pickOrderLineId === currentLot.pickOrderLineId) {
  920. return {
  921. ...lot,
  922. stockOutLineStatus: 'checked',
  923. stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty,
  924. };
  925. }
  926. return lot;
  927. }));
  928. console.log("✅ Lot substitution completed successfully");
  929. setQrScanSuccess(true);
  930. setQrScanError(false);
  931. // 关闭手动输入模态框
  932. setManualLotConfirmationOpen(false);
  933. // 刷新数据
  934. await fetchAllCombinedLotData();
  935. } else if (res.code === "LOT_NUMBER_MISMATCH") {
  936. console.warn("⚠️ Backend reported LOT_NUMBER_MISMATCH:", res.message);
  937. // ✅ 打开 lot confirmation modal 而不是显示 alert
  938. // 从响应消息中提取 expected lot number(如果可能)
  939. // 或者使用 currentLotNo 作为 expected lot
  940. const expectedLotNo = currentLotNo; // 当前 lot 是期望的
  941. // 查找新 lot 的信息(如果存在于 combinedLotData 中)
  942. const newLot = combinedLotData.find(lot =>
  943. lot.lotNo && lot.lotNo === newLotNo
  944. );
  945. // 设置 expected lot data
  946. setExpectedLotData({
  947. lotNo: expectedLotNo,
  948. itemCode: currentLot.itemCode || '',
  949. itemName: currentLot.itemName || ''
  950. });
  951. // 设置 scanned lot data
  952. setScannedLotData({
  953. lotNo: newLotNo,
  954. itemCode: newLot?.itemCode || currentLot.itemCode || '',
  955. itemName: newLot?.itemName || currentLot.itemName || '',
  956. inventoryLotLineId: newLot?.lotId || null,
  957. stockInLineId: null // 手动输入时可能没有 stockInLineId
  958. });
  959. // 设置 selectedLotForQr 为当前 lot
  960. setSelectedLotForQr(currentLot);
  961. // 关闭手动输入模态框
  962. setManualLotConfirmationOpen(false);
  963. // 打开 lot confirmation modal
  964. setLotConfirmationOpen(true);
  965. setQrScanError(false); // 不显示错误,因为会打开确认模态框
  966. setQrScanSuccess(false);
  967. } else if (res.code === "ITEM_MISMATCH") {
  968. console.warn("⚠️ Backend reported ITEM_MISMATCH:", res.message);
  969. alert(t("Item mismatch: {message}", { message: res.message || "" }));
  970. setQrScanError(true);
  971. setQrScanSuccess(false);
  972. // 关闭手动输入模态框
  973. setManualLotConfirmationOpen(false);
  974. } else {
  975. console.warn("⚠️ Unexpected response code:", res.code);
  976. alert(t("Failed to update lot status. Response: {code}", { code: res.code }));
  977. setQrScanError(true);
  978. setQrScanSuccess(false);
  979. // 关闭手动输入模态框
  980. setManualLotConfirmationOpen(false);
  981. }
  982. } catch (error) {
  983. console.error("❌ Error in manual lot confirmation:", error);
  984. alert(t("Failed to confirm lot substitution. Please try again."));
  985. setQrScanError(true);
  986. setQrScanSuccess(false);
  987. // 关闭手动输入模态框
  988. setManualLotConfirmationOpen(false);
  989. } finally {
  990. setIsConfirmingLot(false);
  991. }
  992. }, [combinedLotData, fetchAllCombinedLotData, t]);
  993. useEffect(() => {
  994. if (combinedLotData.length > 0) {
  995. checkAllLotsCompleted(combinedLotData);
  996. }
  997. }, [combinedLotData, checkAllLotsCompleted]);
  998. // Add function to expose completion status to parent
  999. const getCompletionStatus = useCallback(() => {
  1000. return allLotsCompleted;
  1001. }, [allLotsCompleted]);
  1002. // Expose completion status to parent component
  1003. useEffect(() => {
  1004. // Dispatch custom event with completion status
  1005. const event = new CustomEvent('pickOrderCompletionStatus', {
  1006. detail: {
  1007. allLotsCompleted,
  1008. tabIndex: 1 // 明确指定这是来自标签页 1 的事件
  1009. }
  1010. });
  1011. window.dispatchEvent(event);
  1012. }, [allLotsCompleted]);
  1013. const clearLotConfirmationState = useCallback((clearProcessedRefs: boolean = false) => {
  1014. setLotConfirmationOpen(false);
  1015. setExpectedLotData(null);
  1016. setScannedLotData(null);
  1017. setSelectedLotForQr(null);
  1018. if (clearProcessedRefs) {
  1019. setTimeout(() => {
  1020. lastProcessedQrRef.current = '';
  1021. processedQrCodesRef.current.clear();
  1022. console.log(`⏱️ [LOT CONFIRM MODAL] Cleared refs to allow reprocessing`);
  1023. }, 100);
  1024. }
  1025. }, []);
  1026. const parseQrPayload = useCallback((rawQr: string): { itemId: number; stockInLineId: number } | null => {
  1027. if (!rawQr) return null;
  1028. if ((rawQr.startsWith("{2fitest") || rawQr.startsWith("{2fittest")) && rawQr.endsWith("}")) {
  1029. let content = '';
  1030. if (rawQr.startsWith("{2fittest")) {
  1031. content = rawQr.substring(9, rawQr.length - 1);
  1032. } else {
  1033. content = rawQr.substring(8, rawQr.length - 1);
  1034. }
  1035. const parts = content.split(',');
  1036. if (parts.length === 2) {
  1037. const itemId = parseInt(parts[0].trim(), 10);
  1038. const stockInLineId = parseInt(parts[1].trim(), 10);
  1039. if (!isNaN(itemId) && !isNaN(stockInLineId)) {
  1040. return { itemId, stockInLineId };
  1041. }
  1042. }
  1043. return null;
  1044. }
  1045. try {
  1046. const parsed = JSON.parse(rawQr);
  1047. if (parsed?.itemId && parsed?.stockInLineId) {
  1048. return { itemId: parsed.itemId, stockInLineId: parsed.stockInLineId };
  1049. }
  1050. return null;
  1051. } catch {
  1052. return null;
  1053. }
  1054. }, []);
  1055. const handleLotConfirmation = useCallback(async () => {
  1056. if (!expectedLotData || !scannedLotData || !selectedLotForQr) return;
  1057. setIsConfirmingLot(true);
  1058. try {
  1059. const newLotNo = scannedLotData?.lotNo;
  1060. const newStockInLineId = scannedLotData?.stockInLineId;
  1061. await confirmLotSubstitution({
  1062. pickOrderLineId: selectedLotForQr.pickOrderLineId,
  1063. stockOutLineId: selectedLotForQr.stockOutLineId,
  1064. originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId,
  1065. newInventoryLotNo: "",
  1066. newStockInLineId: newStockInLineId
  1067. });
  1068. setQrScanError(false);
  1069. setQrScanSuccess(false);
  1070. setQrScanInput('');
  1071. // ✅ 修复:在确认后重置扫描状态,避免重复处理
  1072. resetScan();
  1073. // ✅ 修复:不要清空 processedQrCodes,而是保留当前 QR code 的标记
  1074. // 或者如果确实需要清空,应该在重置扫描后再清空
  1075. // setProcessedQrCodes(new Set());
  1076. // setLastProcessedQr('');
  1077. setPickExecutionFormOpen(false);
  1078. if(selectedLotForQr?.stockOutLineId){
  1079. const stockOutLineUpdate = await updateStockOutLineStatus({
  1080. id: selectedLotForQr.stockOutLineId,
  1081. status: 'checked',
  1082. qty: 0
  1083. });
  1084. }
  1085. // ✅ 修复:先关闭 modal 和清空状态,再刷新数据
  1086. clearLotConfirmationState(false);
  1087. // ✅ 修复:刷新数据前设置刷新标志,避免在刷新期间处理新的 QR code
  1088. setIsRefreshingData(true);
  1089. await fetchAllCombinedLotData();
  1090. setIsRefreshingData(false);
  1091. } catch (error) {
  1092. console.error("Error confirming lot substitution:", error);
  1093. } finally {
  1094. setIsConfirmingLot(false);
  1095. }
  1096. }, [expectedLotData, scannedLotData, selectedLotForQr, fetchAllCombinedLotData, resetScan, clearLotConfirmationState]);
  1097. const handleLotConfirmationByRescan = useCallback(async (rawQr: string): Promise<boolean> => {
  1098. if (!lotConfirmationOpen || !selectedLotForQr || !expectedLotData || !scannedLotData) {
  1099. return false;
  1100. }
  1101. const payload = parseQrPayload(rawQr);
  1102. const expectedStockInLineId = Number(selectedLotForQr.stockInLineId);
  1103. const mismatchedStockInLineId = Number(scannedLotData?.stockInLineId);
  1104. if (payload) {
  1105. const rescannedStockInLineId = Number(payload.stockInLineId);
  1106. // 再扫“差异 lot” => 直接执行切换
  1107. if (
  1108. Number.isFinite(mismatchedStockInLineId) &&
  1109. rescannedStockInLineId === mismatchedStockInLineId
  1110. ) {
  1111. await handleLotConfirmation();
  1112. return true;
  1113. }
  1114. // 再扫“原建议 lot” => 关闭弹窗并按原 lot 正常记一次扫描
  1115. if (
  1116. Number.isFinite(expectedStockInLineId) &&
  1117. rescannedStockInLineId === expectedStockInLineId
  1118. ) {
  1119. clearLotConfirmationState(false);
  1120. if (processOutsideQrCodeRef.current) {
  1121. await processOutsideQrCodeRef.current(JSON.stringify(payload));
  1122. }
  1123. return true;
  1124. }
  1125. } else {
  1126. // 兼容纯 lotNo 文本扫码
  1127. const scannedText = rawQr?.trim();
  1128. const expectedLotNo = expectedLotData?.lotNo?.trim();
  1129. const mismatchedLotNo = scannedLotData?.lotNo?.trim();
  1130. if (mismatchedLotNo && scannedText === mismatchedLotNo) {
  1131. await handleLotConfirmation();
  1132. return true;
  1133. }
  1134. if (expectedLotNo && scannedText === expectedLotNo) {
  1135. clearLotConfirmationState(false);
  1136. if (processOutsideQrCodeRef.current) {
  1137. await processOutsideQrCodeRef.current(JSON.stringify({
  1138. itemId: selectedLotForQr.itemId,
  1139. stockInLineId: selectedLotForQr.stockInLineId,
  1140. }));
  1141. }
  1142. return true;
  1143. }
  1144. }
  1145. return false;
  1146. }, [lotConfirmationOpen, selectedLotForQr, expectedLotData, scannedLotData, parseQrPayload, handleLotConfirmation, clearLotConfirmationState]);
  1147. useEffect(() => {
  1148. if (!lotConfirmationOpen || !expectedLotData || !scannedLotData || !selectedLotForQr) {
  1149. autoConfirmInProgressRef.current = false;
  1150. return;
  1151. }
  1152. if (autoConfirmInProgressRef.current || isConfirmingLot) {
  1153. return;
  1154. }
  1155. autoConfirmInProgressRef.current = true;
  1156. handleLotConfirmation()
  1157. .catch((error) => {
  1158. console.error("Auto confirm lot substitution failed:", error);
  1159. })
  1160. .finally(() => {
  1161. autoConfirmInProgressRef.current = false;
  1162. });
  1163. }, [lotConfirmationOpen, expectedLotData, scannedLotData, selectedLotForQr, isConfirmingLot, handleLotConfirmation]);
  1164. useEffect(() => {
  1165. if (lotConfirmationOpen) {
  1166. // 记录弹窗打开时的扫码数量,避免把“触发弹窗的同一次扫码”当作二次确认
  1167. lotConfirmOpenedQrCountRef.current = qrValues.length;
  1168. lotConfirmOpenedQrValueRef.current = qrValues[qrValues.length - 1] || '';
  1169. lotConfirmInitialSameQrSkippedRef.current = true;
  1170. }
  1171. }, [lotConfirmationOpen, qrValues.length]);
  1172. const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
  1173. console.log(` Processing QR Code for lot: ${lotNo}`);
  1174. // 检查 lotNo 是否为 null 或 undefined(包括字符串 "null")
  1175. if (!lotNo || lotNo === 'null' || lotNo.trim() === '') {
  1176. console.error(" Invalid lotNo: null, undefined, or empty");
  1177. return;
  1178. }
  1179. // Use current data without refreshing to avoid infinite loop
  1180. const currentLotData = combinedLotData;
  1181. console.log(` Available lots:`, currentLotData.map(lot => lot.lotNo));
  1182. // 修复:在比较前确保 lotNo 不为 null
  1183. const lotNoLower = lotNo.toLowerCase();
  1184. const matchingLots = currentLotData.filter(lot => {
  1185. if (!lot.lotNo) return false; // 跳过 null lotNo
  1186. return lot.lotNo === lotNo || lot.lotNo.toLowerCase() === lotNoLower;
  1187. });
  1188. if (matchingLots.length === 0) {
  1189. console.error(` Lot not found: ${lotNo}`);
  1190. setQrScanError(true);
  1191. setQrScanSuccess(false);
  1192. const availableLotNos = currentLotData.map(lot => lot.lotNo).join(', ');
  1193. console.log(` QR Code "${lotNo}" does not match any expected lots. Available lots: ${availableLotNos}`);
  1194. return;
  1195. }
  1196. console.log(` Found ${matchingLots.length} matching lots:`, matchingLots);
  1197. setQrScanError(false);
  1198. try {
  1199. let successCount = 0;
  1200. let errorCount = 0;
  1201. for (const matchingLot of matchingLots) {
  1202. console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`);
  1203. if (matchingLot.stockOutLineId) {
  1204. const stockOutLineUpdate = await updateStockOutLineStatus({
  1205. id: matchingLot.stockOutLineId,
  1206. status: 'checked',
  1207. qty: 0
  1208. });
  1209. console.log(`Update stock out line result for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate);
  1210. // Treat multiple backend shapes as success (type-safe via any)
  1211. const r: any = stockOutLineUpdate as any;
  1212. const updateOk =
  1213. r?.code === 'SUCCESS' ||
  1214. typeof r?.id === 'number' ||
  1215. r?.type === 'checked' ||
  1216. r?.status === 'checked' ||
  1217. typeof r?.entity?.id === 'number' ||
  1218. r?.entity?.status === 'checked';
  1219. if (updateOk) {
  1220. successCount++;
  1221. } else {
  1222. errorCount++;
  1223. }
  1224. } else {
  1225. const createStockOutLineData = {
  1226. consoCode: matchingLot.pickOrderConsoCode,
  1227. pickOrderLineId: matchingLot.pickOrderLineId,
  1228. inventoryLotLineId: matchingLot.lotId,
  1229. qty: 0
  1230. };
  1231. const createResult = await createStockOutLine(createStockOutLineData);
  1232. console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, createResult);
  1233. if (createResult && createResult.code === "SUCCESS") {
  1234. // Immediately set status to checked for new line
  1235. let newSolId: number | undefined;
  1236. const anyRes: any = createResult as any;
  1237. if (typeof anyRes?.id === 'number') {
  1238. newSolId = anyRes.id;
  1239. } else if (anyRes?.entity) {
  1240. newSolId = Array.isArray(anyRes.entity) ? anyRes.entity[0]?.id : anyRes.entity?.id;
  1241. }
  1242. if (newSolId) {
  1243. const setChecked = await updateStockOutLineStatus({
  1244. id: newSolId,
  1245. status: 'checked',
  1246. qty: 0
  1247. });
  1248. if (setChecked && setChecked.code === "SUCCESS") {
  1249. successCount++;
  1250. } else {
  1251. errorCount++;
  1252. }
  1253. } else {
  1254. console.warn("Created stock out line but no ID returned; cannot set to checked");
  1255. errorCount++;
  1256. }
  1257. } else {
  1258. errorCount++;
  1259. }
  1260. }
  1261. }
  1262. // FIXED: Set refresh flag before refreshing data
  1263. setIsRefreshingData(true);
  1264. console.log("🔄 Refreshing data after QR code processing...");
  1265. await fetchAllCombinedLotData();
  1266. if (successCount > 0) {
  1267. console.log(` QR Code processing completed: ${successCount} updated/created`);
  1268. setQrScanSuccess(true);
  1269. setQrScanError(false);
  1270. setQrScanInput(''); // Clear input after successful processing
  1271. //setIsManualScanning(false);
  1272. // stopScan();
  1273. // resetScan();
  1274. // Clear success state after a delay
  1275. //setTimeout(() => {
  1276. //setQrScanSuccess(false);
  1277. //}, 2000);
  1278. } else {
  1279. console.error(` QR Code processing failed: ${errorCount} errors`);
  1280. setQrScanError(true);
  1281. setQrScanSuccess(false);
  1282. // Clear error state after a delay
  1283. // setTimeout(() => {
  1284. // setQrScanError(false);
  1285. //}, 3000);
  1286. }
  1287. } catch (error) {
  1288. console.error(" Error processing QR code:", error);
  1289. setQrScanError(true);
  1290. setQrScanSuccess(false);
  1291. // Clear error state after a delay
  1292. setTimeout(() => {
  1293. setQrScanError(false);
  1294. }, 3000);
  1295. } finally {
  1296. // Clear refresh flag after a short delay
  1297. setTimeout(() => {
  1298. setIsRefreshingData(false);
  1299. }, 1000);
  1300. }
  1301. }, [combinedLotData]);
  1302. const handleFastQrScan = useCallback(async (lotNo: string) => {
  1303. const startTime = performance.now();
  1304. console.log(`⏱️ [FAST SCAN START] Lot: ${lotNo}`);
  1305. console.log(`⏰ Start time: ${new Date().toISOString()}`);
  1306. // 从 combinedLotData 中找到对应的 lot
  1307. const findStartTime = performance.now();
  1308. const matchingLot = combinedLotData.find(lot =>
  1309. lot.lotNo && lot.lotNo === lotNo
  1310. );
  1311. const findTime = performance.now() - findStartTime;
  1312. console.log(`⏱️ Find lot time: ${findTime.toFixed(2)}ms`);
  1313. if (!matchingLot || !matchingLot.stockOutLineId) {
  1314. const totalTime = performance.now() - startTime;
  1315. console.warn(`⚠️ Fast scan: Lot ${lotNo} not found or no stockOutLineId`);
  1316. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms`);
  1317. return;
  1318. }
  1319. try {
  1320. // ✅ 使用快速 API
  1321. const apiStartTime = performance.now();
  1322. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  1323. pickOrderLineId: matchingLot.pickOrderLineId,
  1324. inventoryLotNo: lotNo,
  1325. stockOutLineId: matchingLot.stockOutLineId,
  1326. itemId: matchingLot.itemId,
  1327. status: "checked",
  1328. });
  1329. const apiTime = performance.now() - apiStartTime;
  1330. console.log(`⏱️ API call time: ${apiTime.toFixed(2)}ms`);
  1331. if (res.code === "checked" || res.code === "SUCCESS") {
  1332. // ✅ 只更新本地状态,不调用 fetchAllCombinedLotData
  1333. const updateStartTime = performance.now();
  1334. const entity = res.entity as any;
  1335. setCombinedLotData(prev => prev.map(lot => {
  1336. if (lot.stockOutLineId === matchingLot.stockOutLineId &&
  1337. lot.pickOrderLineId === matchingLot.pickOrderLineId) {
  1338. return {
  1339. ...lot,
  1340. stockOutLineStatus: 'checked',
  1341. stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty,
  1342. };
  1343. }
  1344. return lot;
  1345. }));
  1346. setOriginalCombinedData(prev => prev.map(lot => {
  1347. if (lot.stockOutLineId === matchingLot.stockOutLineId &&
  1348. lot.pickOrderLineId === matchingLot.pickOrderLineId) {
  1349. return {
  1350. ...lot,
  1351. stockOutLineStatus: 'checked',
  1352. stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty,
  1353. };
  1354. }
  1355. return lot;
  1356. }));
  1357. const updateTime = performance.now() - updateStartTime;
  1358. console.log(`⏱️ State update time: ${updateTime.toFixed(2)}ms`);
  1359. const totalTime = performance.now() - startTime;
  1360. console.log(`✅ [FAST SCAN END] Lot: ${lotNo}`);
  1361. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  1362. console.log(`⏰ End time: ${new Date().toISOString()}`);
  1363. } else {
  1364. const totalTime = performance.now() - startTime;
  1365. console.warn(`⚠️ Fast scan failed for ${lotNo}:`, res.code);
  1366. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms`);
  1367. }
  1368. } catch (error) {
  1369. const totalTime = performance.now() - startTime;
  1370. console.error(` Fast scan error for ${lotNo}:`, error);
  1371. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms`);
  1372. }
  1373. }, [combinedLotData, updateStockOutLineStatusByQRCodeAndLotNo]);
  1374. // Enhanced lotDataIndexes with cached active lots for better performance
  1375. const lotDataIndexes = useMemo(() => {
  1376. const indexStartTime = performance.now();
  1377. console.log(`⏱️ [PERF] lotDataIndexes calculation START, data length: ${combinedLotData.length}`);
  1378. const byItemId = new Map<number, any[]>();
  1379. const byItemCode = new Map<string, any[]>();
  1380. const byLotId = new Map<number, any>();
  1381. const byLotNo = new Map<string, any[]>();
  1382. const byStockInLineId = new Map<number, any[]>();
  1383. // Cache active lots separately to avoid filtering on every scan
  1384. const activeLotsByItemId = new Map<number, any[]>();
  1385. const rejectedStatuses = new Set(['rejected']);
  1386. // ✅ Use for loop instead of forEach for better performance on tablets
  1387. for (let i = 0; i < combinedLotData.length; i++) {
  1388. const lot = combinedLotData[i];
  1389. const isActive = !rejectedStatuses.has(lot.lotAvailability) &&
  1390. !rejectedStatuses.has(lot.stockOutLineStatus) &&
  1391. !rejectedStatuses.has(lot.processingStatus);
  1392. if (lot.itemId) {
  1393. if (!byItemId.has(lot.itemId)) {
  1394. byItemId.set(lot.itemId, []);
  1395. activeLotsByItemId.set(lot.itemId, []);
  1396. }
  1397. byItemId.get(lot.itemId)!.push(lot);
  1398. if (isActive) {
  1399. activeLotsByItemId.get(lot.itemId)!.push(lot);
  1400. }
  1401. }
  1402. if (lot.itemCode) {
  1403. if (!byItemCode.has(lot.itemCode)) {
  1404. byItemCode.set(lot.itemCode, []);
  1405. }
  1406. byItemCode.get(lot.itemCode)!.push(lot);
  1407. }
  1408. if (lot.lotId) {
  1409. byLotId.set(lot.lotId, lot);
  1410. }
  1411. if (lot.lotNo) {
  1412. if (!byLotNo.has(lot.lotNo)) {
  1413. byLotNo.set(lot.lotNo, []);
  1414. }
  1415. byLotNo.get(lot.lotNo)!.push(lot);
  1416. }
  1417. if (lot.stockInLineId) {
  1418. if (!byStockInLineId.has(lot.stockInLineId)) {
  1419. byStockInLineId.set(lot.stockInLineId, []);
  1420. }
  1421. byStockInLineId.get(lot.stockInLineId)!.push(lot);
  1422. }
  1423. }
  1424. const indexTime = performance.now() - indexStartTime;
  1425. if (indexTime > 10) {
  1426. console.log(`⏱️ [PERF] lotDataIndexes calculation END: ${indexTime.toFixed(2)}ms (${(indexTime / 1000).toFixed(3)}s)`);
  1427. }
  1428. return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId, activeLotsByItemId };
  1429. }, [combinedLotData.length, combinedLotData]);
  1430. // Store resetScan in ref for immediate access (update on every render)
  1431. resetScanRef.current = resetScan;
  1432. const processOutsideQrCode = useCallback(async (latestQr: string) => {
  1433. const totalStartTime = performance.now();
  1434. console.log(`⏱️ [PROCESS OUTSIDE QR START] QR: ${latestQr.substring(0, 50)}...`);
  1435. console.log(`⏰ Start time: ${new Date().toISOString()}`);
  1436. // ✅ Measure index access time
  1437. const indexAccessStart = performance.now();
  1438. const indexes = lotDataIndexes; // Access the memoized indexes
  1439. const indexAccessTime = performance.now() - indexAccessStart;
  1440. console.log(`⏱️ [PERF] Index access time: ${indexAccessTime.toFixed(2)}ms`);
  1441. // 1) Parse JSON safely (parse once, reuse)
  1442. const parseStartTime = performance.now();
  1443. let qrData: any = null;
  1444. let parseTime = 0;
  1445. try {
  1446. qrData = JSON.parse(latestQr);
  1447. parseTime = performance.now() - parseStartTime;
  1448. console.log(`⏱️ [PERF] JSON parse time: ${parseTime.toFixed(2)}ms`);
  1449. } catch {
  1450. console.log("QR content is not JSON; skipping lotNo direct submit to avoid false matches.");
  1451. startTransition(() => {
  1452. setQrScanError(true);
  1453. setQrScanSuccess(false);
  1454. });
  1455. return;
  1456. }
  1457. try {
  1458. const validationStartTime = performance.now();
  1459. if (!(qrData?.stockInLineId && qrData?.itemId)) {
  1460. console.log("QR JSON missing required fields (itemId, stockInLineId).");
  1461. startTransition(() => {
  1462. setQrScanError(true);
  1463. setQrScanSuccess(false);
  1464. });
  1465. return;
  1466. }
  1467. const validationTime = performance.now() - validationStartTime;
  1468. console.log(`⏱️ [PERF] Validation time: ${validationTime.toFixed(2)}ms`);
  1469. const scannedItemId = qrData.itemId;
  1470. const scannedStockInLineId = qrData.stockInLineId;
  1471. // ✅ Check if this combination was already processed
  1472. const duplicateCheckStartTime = performance.now();
  1473. const itemProcessedSet = processedQrCombinations.get(scannedItemId);
  1474. if (itemProcessedSet?.has(scannedStockInLineId)) {
  1475. const duplicateCheckTime = performance.now() - duplicateCheckStartTime;
  1476. console.log(`⏱️ [SKIP] Already processed combination: itemId=${scannedItemId}, stockInLineId=${scannedStockInLineId} (check time: ${duplicateCheckTime.toFixed(2)}ms)`);
  1477. return;
  1478. }
  1479. const duplicateCheckTime = performance.now() - duplicateCheckStartTime;
  1480. console.log(`⏱️ [PERF] Duplicate check time: ${duplicateCheckTime.toFixed(2)}ms`);
  1481. // ✅ OPTIMIZATION: Use cached active lots directly (no filtering needed)
  1482. const lookupStartTime = performance.now();
  1483. const activeSuggestedLots = indexes.activeLotsByItemId.get(scannedItemId) || [];
  1484. // ✅ Also get all lots for this item (not just active ones) to allow lot switching even when all lots are rejected
  1485. const allLotsForItem = indexes.byItemId.get(scannedItemId) || [];
  1486. const lookupTime = performance.now() - lookupStartTime;
  1487. console.log(`⏱️ [PERF] Index lookup time: ${lookupTime.toFixed(2)}ms, found ${activeSuggestedLots.length} active lots, ${allLotsForItem.length} total lots`);
  1488. // ✅ Check if scanned lot is rejected BEFORE checking activeSuggestedLots
  1489. // This allows users to scan other lots even when all suggested lots are rejected
  1490. const scannedLot = allLotsForItem.find(
  1491. (lot: any) => lot.stockInLineId === scannedStockInLineId
  1492. );
  1493. if (scannedLot) {
  1494. const isRejected =
  1495. scannedLot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
  1496. scannedLot.lotAvailability === 'rejected' ||
  1497. scannedLot.lotAvailability === 'status_unavailable';
  1498. if (isRejected) {
  1499. console.warn(`⚠️ [QR PROCESS] Scanned lot (stockInLineId: ${scannedStockInLineId}, lotNo: ${scannedLot.lotNo}) is rejected or unavailable`);
  1500. startTransition(() => {
  1501. setQrScanError(true);
  1502. setQrScanSuccess(false);
  1503. setQrScanErrorMsg(
  1504. `此批次(${scannedLot.lotNo || scannedStockInLineId})已被拒绝,无法使用。请扫描其他批次。`
  1505. );
  1506. });
  1507. // Mark as processed to prevent re-processing
  1508. setProcessedQrCombinations(prev => {
  1509. const newMap = new Map(prev);
  1510. if (!newMap.has(scannedItemId)) newMap.set(scannedItemId, new Set());
  1511. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1512. return newMap;
  1513. });
  1514. return;
  1515. }
  1516. }
  1517. // ✅ If no active suggested lots, but scanned lot is not rejected, allow lot switching
  1518. if (activeSuggestedLots.length === 0) {
  1519. // Check if there are any lots for this item (even if all are rejected)
  1520. if (allLotsForItem.length === 0) {
  1521. console.error("No lots found for this item");
  1522. startTransition(() => {
  1523. setQrScanError(true);
  1524. setQrScanSuccess(false);
  1525. setQrScanErrorMsg("当前订单中没有此物品的批次信息");
  1526. });
  1527. return;
  1528. }
  1529. // ✅ Allow lot switching: find a rejected lot as expected lot, or use first lot
  1530. // This allows users to switch to a new lot even when all suggested lots are rejected
  1531. console.log(`⚠️ [QR PROCESS] No active suggested lots, but allowing lot switching.`);
  1532. // Find a rejected lot as expected lot (the one that was rejected)
  1533. const rejectedLot = allLotsForItem.find((lot: any) =>
  1534. lot.stockOutLineStatus?.toLowerCase() === 'rejected' ||
  1535. lot.lotAvailability === 'rejected' ||
  1536. lot.lotAvailability === 'status_unavailable'
  1537. );
  1538. const expectedLot = rejectedLot || allLotsForItem[0]; // Use rejected lot if exists, otherwise first lot
  1539. // ✅ Always open confirmation modal when no active lots (user needs to confirm switching)
  1540. // handleLotMismatch will fetch lotNo from backend using stockInLineId if needed
  1541. console.log(`⚠️ [QR PROCESS] Opening confirmation modal for lot switch (no active lots)`);
  1542. setSelectedLotForQr(expectedLot);
  1543. handleLotMismatch(
  1544. {
  1545. lotNo: expectedLot.lotNo,
  1546. itemCode: expectedLot.itemCode,
  1547. itemName: expectedLot.itemName
  1548. },
  1549. {
  1550. lotNo: scannedLot?.lotNo || null, // Will be fetched by handleLotMismatch if null
  1551. itemCode: expectedLot.itemCode,
  1552. itemName: expectedLot.itemName,
  1553. inventoryLotLineId: scannedLot?.lotId || null,
  1554. stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo
  1555. }
  1556. );
  1557. return;
  1558. }
  1559. // ✅ OPTIMIZATION: Direct Map lookup for stockInLineId match (O(1))
  1560. const matchStartTime = performance.now();
  1561. let exactMatch: any = null;
  1562. const stockInLineLots = indexes.byStockInLineId.get(scannedStockInLineId) || [];
  1563. // Find exact match from stockInLineId index, then verify it's in active lots
  1564. for (let i = 0; i < stockInLineLots.length; i++) {
  1565. const lot = stockInLineLots[i];
  1566. if (lot.itemId === scannedItemId && activeSuggestedLots.includes(lot)) {
  1567. exactMatch = lot;
  1568. break;
  1569. }
  1570. }
  1571. const matchTime = performance.now() - matchStartTime;
  1572. console.log(`⏱️ [PERF] Find exact match time: ${matchTime.toFixed(2)}ms, found: ${exactMatch ? 'yes' : 'no'}`);
  1573. // ✅ Check if scanned lot exists in allLotsForItem but not in activeSuggestedLots
  1574. // This handles the case where Lot A is rejected and user scans Lot B
  1575. // Also handle case where scanned lot is not in allLotsForItem (scannedLot is undefined)
  1576. if (!exactMatch) {
  1577. // Scanned lot is not in active suggested lots, open confirmation modal
  1578. const expectedLot = activeSuggestedLots[0] || allLotsForItem[0]; // Use first active lot or first lot as expected
  1579. if (expectedLot) {
  1580. // Check if scanned lot is different from expected, or if scannedLot is undefined (not in allLotsForItem)
  1581. const shouldOpenModal = !scannedLot || (scannedLot.stockInLineId !== expectedLot.stockInLineId);
  1582. if (shouldOpenModal) {
  1583. console.log(`⚠️ [QR PROCESS] Opening confirmation modal (scanned lot ${scannedLot?.lotNo || 'not in data'} is not in active suggested lots)`);
  1584. setSelectedLotForQr(expectedLot);
  1585. handleLotMismatch(
  1586. {
  1587. lotNo: expectedLot.lotNo,
  1588. itemCode: expectedLot.itemCode,
  1589. itemName: expectedLot.itemName
  1590. },
  1591. {
  1592. lotNo: scannedLot?.lotNo || null, // Will be fetched by handleLotMismatch if null
  1593. itemCode: expectedLot.itemCode,
  1594. itemName: expectedLot.itemName,
  1595. inventoryLotLineId: scannedLot?.lotId || null,
  1596. stockInLineId: scannedStockInLineId // handleLotMismatch will use this to fetch lotNo
  1597. }
  1598. );
  1599. return;
  1600. }
  1601. }
  1602. }
  1603. if (exactMatch) {
  1604. // ✅ Case 1: stockInLineId 匹配 - 直接处理,不需要确认
  1605. console.log(`✅ Exact stockInLineId match found for lot: ${exactMatch.lotNo}`);
  1606. if (!exactMatch.stockOutLineId) {
  1607. console.warn("No stockOutLineId on exactMatch, cannot update status by QR.");
  1608. startTransition(() => {
  1609. setQrScanError(true);
  1610. setQrScanSuccess(false);
  1611. });
  1612. return;
  1613. }
  1614. try {
  1615. const apiStartTime = performance.now();
  1616. console.log(`⏱️ [API CALL START] Calling updateStockOutLineStatusByQRCodeAndLotNo`);
  1617. console.log(`⏰ [API CALL] API start time: ${new Date().toISOString()}`);
  1618. const res = await updateStockOutLineStatusByQRCodeAndLotNo({
  1619. pickOrderLineId: exactMatch.pickOrderLineId,
  1620. inventoryLotNo: exactMatch.lotNo,
  1621. stockOutLineId: exactMatch.stockOutLineId,
  1622. itemId: exactMatch.itemId,
  1623. status: "checked",
  1624. });
  1625. const apiTime = performance.now() - apiStartTime;
  1626. console.log(`⏱️ [API CALL END] Total API time: ${apiTime.toFixed(2)}ms (${(apiTime / 1000).toFixed(3)}s)`);
  1627. console.log(`⏰ [API CALL] API end time: ${new Date().toISOString()}`);
  1628. if (res.code === "checked" || res.code === "SUCCESS") {
  1629. const entity = res.entity as any;
  1630. // ✅ Batch state updates using startTransition
  1631. const stateUpdateStartTime = performance.now();
  1632. startTransition(() => {
  1633. setQrScanError(false);
  1634. setQrScanSuccess(true);
  1635. setCombinedLotData(prev => prev.map(lot => {
  1636. if (lot.stockOutLineId === exactMatch.stockOutLineId &&
  1637. lot.pickOrderLineId === exactMatch.pickOrderLineId) {
  1638. return {
  1639. ...lot,
  1640. stockOutLineStatus: 'checked',
  1641. stockOutLineQty: entity?.qty ?? lot.stockOutLineQty,
  1642. };
  1643. }
  1644. return lot;
  1645. }));
  1646. setOriginalCombinedData(prev => prev.map(lot => {
  1647. if (lot.stockOutLineId === exactMatch.stockOutLineId &&
  1648. lot.pickOrderLineId === exactMatch.pickOrderLineId) {
  1649. return {
  1650. ...lot,
  1651. stockOutLineStatus: 'checked',
  1652. stockOutLineQty: entity?.qty ?? lot.stockOutLineQty,
  1653. };
  1654. }
  1655. return lot;
  1656. }));
  1657. });
  1658. const stateUpdateTime = performance.now() - stateUpdateStartTime;
  1659. console.log(`⏱️ [PERF] State update time: ${stateUpdateTime.toFixed(2)}ms`);
  1660. // Mark this combination as processed
  1661. const markProcessedStartTime = performance.now();
  1662. setProcessedQrCombinations(prev => {
  1663. const newMap = new Map(prev);
  1664. if (!newMap.has(scannedItemId)) {
  1665. newMap.set(scannedItemId, new Set());
  1666. }
  1667. newMap.get(scannedItemId)!.add(scannedStockInLineId);
  1668. return newMap;
  1669. });
  1670. const markProcessedTime = performance.now() - markProcessedStartTime;
  1671. console.log(`⏱️ [PERF] Mark processed time: ${markProcessedTime.toFixed(2)}ms`);
  1672. const totalTime = performance.now() - totalStartTime;
  1673. console.log(`✅ [PROCESS OUTSIDE QR END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  1674. console.log(`⏰ End time: ${new Date().toISOString()}`);
  1675. 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`);
  1676. console.log("✅ Status updated locally, no full data refresh needed");
  1677. } else {
  1678. console.warn("Unexpected response code from backend:", res.code);
  1679. startTransition(() => {
  1680. setQrScanError(true);
  1681. setQrScanSuccess(false);
  1682. });
  1683. }
  1684. } catch (e) {
  1685. const totalTime = performance.now() - totalStartTime;
  1686. console.error(`❌ [PROCESS OUTSIDE QR ERROR] Total time: ${totalTime.toFixed(2)}ms`);
  1687. console.error("Error calling updateStockOutLineStatusByQRCodeAndLotNo:", e);
  1688. startTransition(() => {
  1689. setQrScanError(true);
  1690. setQrScanSuccess(false);
  1691. });
  1692. }
  1693. return; // ✅ 直接返回,不需要确认表单
  1694. }
  1695. // ✅ Case 2: itemId 匹配但 stockInLineId 不匹配 - 显示确认表单
  1696. // Check if we should allow reopening (different stockInLineId)
  1697. const mismatchCheckStartTime = performance.now();
  1698. const itemProcessedSet2 = processedQrCombinations.get(scannedItemId);
  1699. if (itemProcessedSet2?.has(scannedStockInLineId)) {
  1700. const mismatchCheckTime = performance.now() - mismatchCheckStartTime;
  1701. console.log(`⏱️ [SKIP] Already processed this exact combination (check time: ${mismatchCheckTime.toFixed(2)}ms)`);
  1702. return;
  1703. }
  1704. const mismatchCheckTime = performance.now() - mismatchCheckStartTime;
  1705. console.log(`⏱️ [PERF] Mismatch check time: ${mismatchCheckTime.toFixed(2)}ms`);
  1706. // 取第一个活跃的 lot 作为期望的 lot
  1707. const expectedLotStartTime = performance.now();
  1708. const expectedLot = activeSuggestedLots[0];
  1709. if (!expectedLot) {
  1710. console.error("Could not determine expected lot for confirmation");
  1711. startTransition(() => {
  1712. setQrScanError(true);
  1713. setQrScanSuccess(false);
  1714. });
  1715. return;
  1716. }
  1717. const expectedLotTime = performance.now() - expectedLotStartTime;
  1718. console.log(`⏱️ [PERF] Get expected lot time: ${expectedLotTime.toFixed(2)}ms`);
  1719. // ✅ 立即打开确认模态框,不等待其他操作
  1720. console.log(`⚠️ Lot mismatch: Expected stockInLineId=${expectedLot.stockInLineId}, Scanned stockInLineId=${scannedStockInLineId}`);
  1721. // Set selected lot immediately (no transition delay)
  1722. const setSelectedLotStartTime = performance.now();
  1723. setSelectedLotForQr(expectedLot);
  1724. const setSelectedLotTime = performance.now() - setSelectedLotStartTime;
  1725. console.log(`⏱️ [PERF] Set selected lot time: ${setSelectedLotTime.toFixed(2)}ms`);
  1726. // ✅ 获取扫描的 lot 信息(从 QR 数据中提取,或使用默认值)
  1727. // Call handleLotMismatch immediately - it will open the modal
  1728. const handleMismatchStartTime = performance.now();
  1729. handleLotMismatch(
  1730. {
  1731. lotNo: expectedLot.lotNo,
  1732. itemCode: expectedLot.itemCode,
  1733. itemName: expectedLot.itemName
  1734. },
  1735. {
  1736. lotNo: null, // 扫描的 lotNo 未知,需要从后端获取或显示为未知
  1737. itemCode: expectedLot.itemCode,
  1738. itemName: expectedLot.itemName,
  1739. inventoryLotLineId: null,
  1740. stockInLineId: scannedStockInLineId // ✅ 传递 stockInLineId
  1741. }
  1742. );
  1743. const handleMismatchTime = performance.now() - handleMismatchStartTime;
  1744. console.log(`⏱️ [PERF] Handle mismatch call time: ${handleMismatchTime.toFixed(2)}ms`);
  1745. const totalTime = performance.now() - totalStartTime;
  1746. console.log(`⚠️ [PROCESS OUTSIDE QR MISMATCH] Total time before modal: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  1747. console.log(`⏰ End time: ${new Date().toISOString()}`);
  1748. 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`);
  1749. } catch (error) {
  1750. const totalTime = performance.now() - totalStartTime;
  1751. console.error(`❌ [PROCESS OUTSIDE QR ERROR] Total time: ${totalTime.toFixed(2)}ms`);
  1752. console.error("Error during QR code processing:", error);
  1753. startTransition(() => {
  1754. setQrScanError(true);
  1755. setQrScanSuccess(false);
  1756. });
  1757. return;
  1758. }
  1759. }, [lotDataIndexes, handleLotMismatch, processedQrCombinations, combinedLotData, fetchStockInLineInfoCached]);
  1760. // Store processOutsideQrCode in ref for immediate access (update on every render)
  1761. processOutsideQrCodeRef.current = processOutsideQrCode;
  1762. useEffect(() => {
  1763. // Skip if scanner is not active or no data available
  1764. if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) {
  1765. return;
  1766. }
  1767. const qrValuesChangeStartTime = performance.now();
  1768. console.log(`⏱️ [QR VALUES EFFECT] Triggered at: ${new Date().toISOString()}`);
  1769. console.log(`⏱️ [QR VALUES EFFECT] qrValues.length: ${qrValues.length}`);
  1770. console.log(`⏱️ [QR VALUES EFFECT] qrValues:`, qrValues);
  1771. const latestQr = qrValues[qrValues.length - 1];
  1772. console.log(`⏱️ [QR VALUES EFFECT] Latest QR: ${latestQr}`);
  1773. console.log(`⏰ [QR VALUES EFFECT] Latest QR detected at: ${new Date().toISOString()}`);
  1774. // ✅ FIXED: Handle test shortcut {2fitestx,y} or {2fittestx,y} where x=itemId, y=stockInLineId
  1775. // Support both formats: {2fitest (2 t's) and {2fittest (3 t's)
  1776. if ((latestQr.startsWith("{2fitest") || latestQr.startsWith("{2fittest")) && latestQr.endsWith("}")) {
  1777. // Extract content: remove "{2fitest" or "{2fittest" and "}"
  1778. let content = '';
  1779. if (latestQr.startsWith("{2fittest")) {
  1780. content = latestQr.substring(9, latestQr.length - 1); // Remove "{2fittest" and "}"
  1781. } else if (latestQr.startsWith("{2fitest")) {
  1782. content = latestQr.substring(8, latestQr.length - 1); // Remove "{2fitest" and "}"
  1783. }
  1784. const parts = content.split(',');
  1785. if (parts.length === 2) {
  1786. const itemId = parseInt(parts[0].trim(), 10);
  1787. const stockInLineId = parseInt(parts[1].trim(), 10);
  1788. if (!isNaN(itemId) && !isNaN(stockInLineId)) {
  1789. console.log(
  1790. `%c TEST QR: Detected ${latestQr.substring(0, 9)}... - Simulating QR input (itemId=${itemId}, stockInLineId=${stockInLineId})`,
  1791. "color: purple; font-weight: bold"
  1792. );
  1793. // ✅ Simulate QR code JSON format
  1794. const simulatedQr = JSON.stringify({
  1795. itemId: itemId,
  1796. stockInLineId: stockInLineId
  1797. });
  1798. console.log(`⏱️ [TEST QR] Simulated QR content: ${simulatedQr}`);
  1799. console.log(`⏱️ [TEST QR] Start time: ${new Date().toISOString()}`);
  1800. const testStartTime = performance.now();
  1801. // ✅ Mark as processed FIRST to avoid duplicate processing
  1802. lastProcessedQrRef.current = latestQr;
  1803. processedQrCodesRef.current.add(latestQr);
  1804. if (processedQrCodesRef.current.size > 100) {
  1805. const firstValue = processedQrCodesRef.current.values().next().value;
  1806. if (firstValue !== undefined) {
  1807. processedQrCodesRef.current.delete(firstValue);
  1808. }
  1809. }
  1810. setLastProcessedQr(latestQr);
  1811. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1812. // ✅ Process immediately (bypass QR scanner delay)
  1813. if (processOutsideQrCodeRef.current) {
  1814. processOutsideQrCodeRef.current(simulatedQr).then(() => {
  1815. const testTime = performance.now() - testStartTime;
  1816. console.log(`⏱️ [TEST QR] Total processing time: ${testTime.toFixed(2)}ms (${(testTime / 1000).toFixed(3)}s)`);
  1817. console.log(`⏱️ [TEST QR] End time: ${new Date().toISOString()}`);
  1818. }).catch((error) => {
  1819. const testTime = performance.now() - testStartTime;
  1820. console.error(`❌ [TEST QR] Error after ${testTime.toFixed(2)}ms:`, error);
  1821. });
  1822. }
  1823. // Reset scan
  1824. if (resetScanRef.current) {
  1825. resetScanRef.current();
  1826. }
  1827. const qrValuesChangeTime = performance.now() - qrValuesChangeStartTime;
  1828. console.log(`⏱️ [QR VALUES EFFECT] Test QR handling time: ${qrValuesChangeTime.toFixed(2)}ms`);
  1829. return; // ✅ IMPORTANT: Return early to prevent normal processing
  1830. } else {
  1831. console.warn(`⏱️ [TEST QR] Invalid itemId or stockInLineId: itemId=${parts[0]}, stockInLineId=${parts[1]}`);
  1832. }
  1833. } else {
  1834. console.warn(`⏱️ [TEST QR] Invalid format. Expected {2fitestx,y} or {2fittestx,y}, got: ${latestQr}`);
  1835. }
  1836. }
  1837. // lot confirm 弹窗打开时,允许通过“再次扫码”决定走向(切换或继续原 lot)
  1838. if (lotConfirmationOpen) {
  1839. // 已改回自动确认:弹窗打开时不再等待二次扫码
  1840. return;
  1841. }
  1842. // Skip processing if manual confirmation modal is open
  1843. if (manualLotConfirmationOpen) {
  1844. // Check if this is a different QR code than what triggered the modal
  1845. const modalTriggerQr = lastProcessedQrRef.current;
  1846. if (latestQr === modalTriggerQr) {
  1847. console.log(`⏱️ [QR PROCESS] Skipping - manual modal open for same QR`);
  1848. return;
  1849. }
  1850. // If it's a different QR, allow processing
  1851. console.log(`⏱️ [QR PROCESS] Different QR detected while manual modal open, allowing processing`);
  1852. }
  1853. const qrDetectionStartTime = performance.now();
  1854. console.log(`⏱️ [QR DETECTION] Latest QR detected: ${latestQr?.substring(0, 50)}...`);
  1855. console.log(`⏰ [QR DETECTION] Detection time: ${new Date().toISOString()}`);
  1856. console.log(`⏱️ [QR DETECTION] Time since QR scanner set value: ${(qrDetectionStartTime - qrValuesChangeStartTime).toFixed(2)}ms`);
  1857. // Skip if already processed (use refs to avoid dependency issues and delays)
  1858. const checkProcessedStartTime = performance.now();
  1859. if (processedQrCodesRef.current.has(latestQr) || lastProcessedQrRef.current === latestQr) {
  1860. const checkTime = performance.now() - checkProcessedStartTime;
  1861. console.log(`⏱️ [QR PROCESS] Already processed check time: ${checkTime.toFixed(2)}ms`);
  1862. return;
  1863. }
  1864. const checkTime = performance.now() - checkProcessedStartTime;
  1865. console.log(` [QR PROCESS] Not processed check time: ${checkTime.toFixed(2)}ms`);
  1866. // Handle special shortcut
  1867. if (latestQr === "{2fic}") {
  1868. console.log(" Detected {2fic} shortcut - opening manual lot confirmation form");
  1869. setManualLotConfirmationOpen(true);
  1870. if (resetScanRef.current) {
  1871. resetScanRef.current();
  1872. }
  1873. lastProcessedQrRef.current = latestQr;
  1874. processedQrCodesRef.current.add(latestQr);
  1875. if (processedQrCodesRef.current.size > 100) {
  1876. const firstValue = processedQrCodesRef.current.values().next().value;
  1877. if (firstValue !== undefined) {
  1878. processedQrCodesRef.current.delete(firstValue);
  1879. }
  1880. }
  1881. setLastProcessedQr(latestQr);
  1882. setProcessedQrCodes(prev => {
  1883. const newSet = new Set(prev);
  1884. newSet.add(latestQr);
  1885. if (newSet.size > 100) {
  1886. const firstValue = newSet.values().next().value;
  1887. if (firstValue !== undefined) {
  1888. newSet.delete(firstValue);
  1889. }
  1890. }
  1891. return newSet;
  1892. });
  1893. return;
  1894. }
  1895. // Process new QR code immediately (background mode - no modal)
  1896. // Check against refs to avoid state update delays
  1897. if (latestQr && latestQr !== lastProcessedQrRef.current) {
  1898. const processingStartTime = performance.now();
  1899. console.log(`⏱️ [QR PROCESS] Starting processing at: ${new Date().toISOString()}`);
  1900. console.log(`⏱️ [QR PROCESS] Time since detection: ${(processingStartTime - qrDetectionStartTime).toFixed(2)}ms`);
  1901. // ✅ Process immediately for better responsiveness
  1902. // Clear any pending debounced processing
  1903. if (qrProcessingTimeoutRef.current) {
  1904. clearTimeout(qrProcessingTimeoutRef.current);
  1905. qrProcessingTimeoutRef.current = null;
  1906. }
  1907. // Log immediately (console.log is synchronous)
  1908. console.log(`⏱️ [QR PROCESS] Processing new QR code with enhanced validation: ${latestQr}`);
  1909. // Update refs immediately (no state update delay) - do this FIRST
  1910. const refUpdateStartTime = performance.now();
  1911. lastProcessedQrRef.current = latestQr;
  1912. processedQrCodesRef.current.add(latestQr);
  1913. if (processedQrCodesRef.current.size > 100) {
  1914. const firstValue = processedQrCodesRef.current.values().next().value;
  1915. if (firstValue !== undefined) {
  1916. processedQrCodesRef.current.delete(firstValue);
  1917. }
  1918. }
  1919. const refUpdateTime = performance.now() - refUpdateStartTime;
  1920. console.log(`⏱️ [QR PROCESS] Ref update time: ${refUpdateTime.toFixed(2)}ms`);
  1921. // Process immediately in background - no modal/form needed, no delays
  1922. // Use ref to avoid dependency issues
  1923. const processCallStartTime = performance.now();
  1924. if (processOutsideQrCodeRef.current) {
  1925. processOutsideQrCodeRef.current(latestQr).then(() => {
  1926. const processCallTime = performance.now() - processCallStartTime;
  1927. const totalProcessingTime = performance.now() - processingStartTime;
  1928. console.log(`⏱️ [QR PROCESS] processOutsideQrCode call time: ${processCallTime.toFixed(2)}ms`);
  1929. console.log(`⏱️ [QR PROCESS] Total processing time: ${totalProcessingTime.toFixed(2)}ms (${(totalProcessingTime / 1000).toFixed(3)}s)`);
  1930. }).catch((error) => {
  1931. const processCallTime = performance.now() - processCallStartTime;
  1932. const totalProcessingTime = performance.now() - processingStartTime;
  1933. console.error(`❌ [QR PROCESS] processOutsideQrCode error after ${processCallTime.toFixed(2)}ms:`, error);
  1934. console.error(`❌ [QR PROCESS] Total processing time before error: ${totalProcessingTime.toFixed(2)}ms`);
  1935. });
  1936. }
  1937. // Update state for UI (but don't block on it)
  1938. const stateUpdateStartTime = performance.now();
  1939. setLastProcessedQr(latestQr);
  1940. setProcessedQrCodes(new Set(processedQrCodesRef.current));
  1941. const stateUpdateTime = performance.now() - stateUpdateStartTime;
  1942. console.log(`⏱️ [QR PROCESS] State update time: ${stateUpdateTime.toFixed(2)}ms`);
  1943. const detectionTime = performance.now() - qrDetectionStartTime;
  1944. const totalEffectTime = performance.now() - qrValuesChangeStartTime;
  1945. console.log(`⏱️ [QR DETECTION] Total detection time: ${detectionTime.toFixed(2)}ms`);
  1946. console.log(`⏱️ [QR VALUES EFFECT] Total effect time: ${totalEffectTime.toFixed(2)}ms`);
  1947. }
  1948. return () => {
  1949. if (qrProcessingTimeoutRef.current) {
  1950. clearTimeout(qrProcessingTimeoutRef.current);
  1951. qrProcessingTimeoutRef.current = null;
  1952. }
  1953. };
  1954. }, [qrValues, isManualScanning, isRefreshingData, combinedLotData.length, lotConfirmationOpen, manualLotConfirmationOpen, handleLotConfirmationByRescan]);
  1955. const renderCountRef = useRef(0);
  1956. const renderStartTimeRef = useRef<number | null>(null);
  1957. // Track render performance
  1958. useEffect(() => {
  1959. renderCountRef.current++;
  1960. const now = performance.now();
  1961. if (renderStartTimeRef.current !== null) {
  1962. const renderTime = now - renderStartTimeRef.current;
  1963. if (renderTime > 100) { // Only log slow renders (>100ms)
  1964. console.log(`⏱️ [PERF] Render #${renderCountRef.current} took ${renderTime.toFixed(2)}ms, combinedLotData length: ${combinedLotData.length}`);
  1965. }
  1966. renderStartTimeRef.current = null;
  1967. }
  1968. // Track when lotConfirmationOpen changes
  1969. if (lotConfirmationOpen) {
  1970. renderStartTimeRef.current = performance.now();
  1971. console.log(`⏱️ [PERF] Render triggered by lotConfirmationOpen=true`);
  1972. }
  1973. }, [combinedLotData.length, lotConfirmationOpen]);
  1974. // Auto-start scanner only once on mount
  1975. const scannerInitializedRef = useRef(false);
  1976. useEffect(() => {
  1977. if (session && currentUserId && !initializationRef.current) {
  1978. console.log(" Session loaded, initializing pick order...");
  1979. initializationRef.current = true;
  1980. // Only fetch existing data, no auto-assignment
  1981. fetchAllCombinedLotData();
  1982. }
  1983. }, [session, currentUserId, fetchAllCombinedLotData]);
  1984. // Separate effect for auto-starting scanner (only once, prevents multiple resets)
  1985. useEffect(() => {
  1986. if (session && currentUserId && !scannerInitializedRef.current) {
  1987. scannerInitializedRef.current = true;
  1988. // ✅ Auto-start scanner on mount for tablet use (background mode - no modal)
  1989. console.log("✅ Auto-starting QR scanner in background mode");
  1990. setIsManualScanning(true);
  1991. startScan();
  1992. }
  1993. }, [session, currentUserId, startScan]);
  1994. // Add event listener for manual assignment
  1995. useEffect(() => {
  1996. const handlePickOrderAssigned = () => {
  1997. console.log("🔄 Pick order assigned event received, refreshing data...");
  1998. fetchAllCombinedLotData();
  1999. };
  2000. window.addEventListener('pickOrderAssigned', handlePickOrderAssigned);
  2001. return () => {
  2002. window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned);
  2003. };
  2004. }, [fetchAllCombinedLotData]);
  2005. const handleManualInputSubmit = useCallback(() => {
  2006. if (qrScanInput.trim() !== '') {
  2007. handleQrCodeSubmit(qrScanInput.trim());
  2008. }
  2009. }, [qrScanInput, handleQrCodeSubmit]);
  2010. // Handle QR code submission from modal (internal scanning)
  2011. const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
  2012. if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
  2013. console.log(` QR Code verified for lot: ${lotNo}`);
  2014. const requiredQty = selectedLotForQr.requiredQty;
  2015. const lotId = selectedLotForQr.lotId;
  2016. // Create stock out line
  2017. try {
  2018. const stockOutLineUpdate = await updateStockOutLineStatus({
  2019. id: selectedLotForQr.stockOutLineId,
  2020. status: 'checked',
  2021. qty: selectedLotForQr.stockOutLineQty || 0
  2022. });
  2023. console.log("Stock out line updated successfully!");
  2024. setQrScanSuccess(true);
  2025. setQrScanError(false);
  2026. // Clear selected lot (scanner stays active)
  2027. setSelectedLotForQr(null);
  2028. // Set pick quantity
  2029. const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`;
  2030. setTimeout(() => {
  2031. setPickQtyData(prev => ({
  2032. ...prev,
  2033. [lotKey]: requiredQty
  2034. }));
  2035. console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
  2036. }, 500);
  2037. } catch (error) {
  2038. console.error("Error creating stock out line:", error);
  2039. }
  2040. }
  2041. }, [selectedLotForQr]);
  2042. const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
  2043. if (value === '' || value === null || value === undefined) {
  2044. setPickQtyData(prev => ({
  2045. ...prev,
  2046. [lotKey]: 0
  2047. }));
  2048. return;
  2049. }
  2050. const numericValue = typeof value === 'string' ? parseFloat(value) : value;
  2051. if (isNaN(numericValue)) {
  2052. setPickQtyData(prev => ({
  2053. ...prev,
  2054. [lotKey]: 0
  2055. }));
  2056. return;
  2057. }
  2058. setPickQtyData(prev => ({
  2059. ...prev,
  2060. [lotKey]: numericValue
  2061. }));
  2062. }, []);
  2063. const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle');
  2064. const [autoAssignMessage, setAutoAssignMessage] = useState<string>('');
  2065. const [completionStatus, setCompletionStatus] = useState<PickOrderCompletionResponse | null>(null);
  2066. const checkAndAutoAssignNext = useCallback(async () => {
  2067. if (!currentUserId) return;
  2068. try {
  2069. const completionResponse = await checkPickOrderCompletion(currentUserId);
  2070. if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) {
  2071. console.log("Found completed pick orders, auto-assigning next...");
  2072. // 移除前端的自动分配逻辑,因为后端已经处理了
  2073. // await handleAutoAssignAndRelease(); // 删除这个函数
  2074. }
  2075. } catch (error) {
  2076. console.error("Error checking pick order completion:", error);
  2077. }
  2078. }, [currentUserId]);
  2079. // Handle reject lot
  2080. // Handle pick execution form
  2081. const handlePickExecutionForm = useCallback((lot: any) => {
  2082. console.log("=== Pick Execution Form ===");
  2083. console.log("Lot data:", lot);
  2084. if (!lot) {
  2085. console.warn("No lot data provided for pick execution form");
  2086. return;
  2087. }
  2088. console.log("Opening pick execution form for lot:", lot.lotNo);
  2089. setSelectedLotForExecutionForm(lot);
  2090. setPickExecutionFormOpen(true);
  2091. console.log("Pick execution form opened for lot ID:", lot.lotId);
  2092. }, []);
  2093. const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
  2094. try {
  2095. console.log("Pick execution form submitted:", data);
  2096. const issueData = {
  2097. ...data,
  2098. type: "Do", // Delivery Order Record 类型
  2099. pickerName: session?.user?.name || '',
  2100. };
  2101. const result = await recordPickExecutionIssue(issueData);
  2102. console.log("Pick execution issue recorded:", result);
  2103. if (result && result.code === "SUCCESS") {
  2104. console.log(" Pick execution issue recorded successfully");
  2105. // 关键:issue form 只记录问题,不会更新 SOL.qty
  2106. // 但 batch submit 需要知道“实际拣到多少”,否则会按 requiredQty 补拣到满
  2107. const solId = Number(issueData.stockOutLineId || issueData.stockOutLineId === 0 ? issueData.stockOutLineId : data?.stockOutLineId);
  2108. if (solId > 0) {
  2109. const picked = Number(issueData.actualPickQty || 0);
  2110. setIssuePickedQtyBySolId(prev => ({ ...prev, [solId]: picked }));
  2111. setCombinedLotData(prev => prev.map(lot => {
  2112. if (Number(lot.stockOutLineId) === solId) {
  2113. return { ...lot, actualPickQty: picked, stockOutLineQty: picked };
  2114. }
  2115. return lot;
  2116. }));
  2117. }
  2118. } else {
  2119. console.error(" Failed to record pick execution issue:", result);
  2120. }
  2121. setPickExecutionFormOpen(false);
  2122. setSelectedLotForExecutionForm(null);
  2123. setQrScanError(false);
  2124. setQrScanSuccess(false);
  2125. setQrScanInput('');
  2126. // ✅ Keep scanner active after form submission - don't stop scanning
  2127. // Only clear processed QR codes for the specific lot, not all
  2128. // setIsManualScanning(false); // Removed - keep scanner active
  2129. // stopScan(); // Removed - keep scanner active
  2130. // resetScan(); // Removed - keep scanner active
  2131. // Don't clear all processed codes - only clear for this specific lot if needed
  2132. await fetchAllCombinedLotData();
  2133. } catch (error) {
  2134. console.error("Error submitting pick execution form:", error);
  2135. }
  2136. }, [fetchAllCombinedLotData, session]);
  2137. // Calculate remaining required quantity
  2138. const calculateRemainingRequiredQty = useCallback((lot: any) => {
  2139. const requiredQty = lot.requiredQty || 0;
  2140. const stockOutLineQty = lot.stockOutLineQty || 0;
  2141. return Math.max(0, requiredQty - stockOutLineQty);
  2142. }, []);
  2143. // Search criteria
  2144. const searchCriteria: Criterion<any>[] = [
  2145. {
  2146. label: t("Pick Order Code"),
  2147. paramName: "pickOrderCode",
  2148. type: "text",
  2149. },
  2150. {
  2151. label: t("Item Code"),
  2152. paramName: "itemCode",
  2153. type: "text",
  2154. },
  2155. {
  2156. label: t("Item Name"),
  2157. paramName: "itemName",
  2158. type: "text",
  2159. },
  2160. {
  2161. label: t("Lot No"),
  2162. paramName: "lotNo",
  2163. type: "text",
  2164. },
  2165. ];
  2166. const handleSearch = useCallback((query: Record<string, any>) => {
  2167. setSearchQuery({ ...query });
  2168. console.log("Search query:", query);
  2169. if (!originalCombinedData) return;
  2170. const filtered = originalCombinedData.filter((lot: any) => {
  2171. const pickOrderCodeMatch = !query.pickOrderCode ||
  2172. lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
  2173. const itemCodeMatch = !query.itemCode ||
  2174. lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
  2175. const itemNameMatch = !query.itemName ||
  2176. lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase());
  2177. const lotNoMatch = !query.lotNo ||
  2178. lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase());
  2179. return pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch;
  2180. });
  2181. setCombinedLotData(filtered);
  2182. console.log("Filtered lots count:", filtered.length);
  2183. }, [originalCombinedData]);
  2184. const handleReset = useCallback(() => {
  2185. setSearchQuery({});
  2186. if (originalCombinedData) {
  2187. setCombinedLotData(originalCombinedData);
  2188. }
  2189. }, [originalCombinedData]);
  2190. const handlePageChange = useCallback((event: unknown, newPage: number) => {
  2191. setPaginationController(prev => ({
  2192. ...prev,
  2193. pageNum: newPage,
  2194. }));
  2195. }, []);
  2196. const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
  2197. const newPageSize = parseInt(event.target.value, 10);
  2198. setPaginationController({
  2199. pageNum: 0,
  2200. pageSize: newPageSize === -1 ? -1 : newPageSize,
  2201. });
  2202. }, []);
  2203. // Pagination data with sorting by routerIndex
  2204. // Remove the sorting logic and just do pagination
  2205. // ✅ Memoize paginatedData to prevent re-renders when modal opens
  2206. const paginatedData = useMemo(() => {
  2207. if (paginationController.pageSize === -1) {
  2208. return combinedLotData; // Show all items
  2209. }
  2210. const startIndex = paginationController.pageNum * paginationController.pageSize;
  2211. const endIndex = startIndex + paginationController.pageSize;
  2212. return combinedLotData.slice(startIndex, endIndex); // No sorting needed
  2213. }, [combinedLotData, paginationController.pageNum, paginationController.pageSize]);
  2214. const allItemsReady = useMemo(() => {
  2215. if (combinedLotData.length === 0) return false;
  2216. return combinedLotData.every((lot: any) => {
  2217. const status = lot.stockOutLineStatus?.toLowerCase();
  2218. const isRejected =
  2219. status === 'rejected' || lot.lotAvailability === 'rejected';
  2220. const isCompleted =
  2221. status === 'completed' || status === 'partially_completed' || status === 'partially_complete';
  2222. const isChecked = status === 'checked';
  2223. const isPending = status === 'pending';
  2224. // ✅ FIXED: 无库存(noLot)行:pending 状态也应该被视为 ready(可以提交)
  2225. if (lot.noLot === true) {
  2226. return isChecked || isCompleted || isRejected || isPending;
  2227. }
  2228. // 正常 lot:必须已扫描/提交或者被拒收
  2229. return isChecked || isCompleted || isRejected;
  2230. });
  2231. }, [combinedLotData]);
  2232. const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
  2233. if (!lot.stockOutLineId) {
  2234. console.error("No stock out line found for this lot");
  2235. return;
  2236. }
  2237. const solId = Number(lot.stockOutLineId);
  2238. if (solId > 0 && actionBusyBySolId[solId]) {
  2239. console.warn("Action already in progress for stockOutLineId:", solId);
  2240. return;
  2241. }
  2242. try {
  2243. if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: true }));
  2244. // Special case: If submitQty is 0 and all values are 0, mark as completed with qty: 0
  2245. if (submitQty === 0) {
  2246. console.log(`=== SUBMITTING ALL ZEROS CASE ===`);
  2247. console.log(`Lot: ${lot.lotNo}`);
  2248. console.log(`Stock Out Line ID: ${lot.stockOutLineId}`);
  2249. console.log(`Setting status to 'completed' with qty: 0`);
  2250. const updateResult = await updateStockOutLineStatus({
  2251. id: lot.stockOutLineId,
  2252. status: 'completed',
  2253. qty: 0
  2254. });
  2255. console.log('Update result:', updateResult);
  2256. const r: any = updateResult as any;
  2257. const updateOk =
  2258. r?.code === 'SUCCESS' ||
  2259. r?.type === 'completed' ||
  2260. typeof r?.id === 'number' ||
  2261. typeof r?.entity?.id === 'number' ||
  2262. (r?.message && r.message.includes('successfully'));
  2263. if (!updateResult || !updateOk) {
  2264. console.error('Failed to update stock out line status:', updateResult);
  2265. throw new Error('Failed to update stock out line status');
  2266. }
  2267. // Check if pick order is completed
  2268. if (lot.pickOrderConsoCode) {
  2269. console.log(` Lot ${lot.lotNo} completed (all zeros), checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  2270. try {
  2271. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  2272. console.log(` Pick order completion check result:`, completionResponse);
  2273. if (completionResponse.code === "SUCCESS") {
  2274. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  2275. } else if (completionResponse.message === "not completed") {
  2276. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  2277. } else {
  2278. console.error(` Error checking completion: ${completionResponse.message}`);
  2279. }
  2280. } catch (error) {
  2281. console.error("Error checking pick order completion:", error);
  2282. }
  2283. }
  2284. await fetchAllCombinedLotData();
  2285. console.log("All zeros submission completed successfully!");
  2286. setTimeout(() => {
  2287. checkAndAutoAssignNext();
  2288. }, 1000);
  2289. return;
  2290. }
  2291. // FIXED: Calculate cumulative quantity correctly
  2292. const currentActualPickQty = lot.actualPickQty || 0;
  2293. const cumulativeQty = currentActualPickQty + submitQty;
  2294. // FIXED: Determine status based on cumulative quantity vs required quantity
  2295. let newStatus = 'partially_completed';
  2296. if (cumulativeQty >= lot.requiredQty) {
  2297. newStatus = 'completed';
  2298. } else if (cumulativeQty > 0) {
  2299. newStatus = 'partially_completed';
  2300. } else {
  2301. newStatus = 'checked'; // QR scanned but no quantity submitted yet
  2302. }
  2303. console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
  2304. console.log(`Lot: ${lot.lotNo}`);
  2305. console.log(`Required Qty: ${lot.requiredQty}`);
  2306. console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
  2307. console.log(`New Submitted Qty: ${submitQty}`);
  2308. console.log(`Cumulative Qty: ${cumulativeQty}`);
  2309. console.log(`New Status: ${newStatus}`);
  2310. console.log(`=====================================`);
  2311. await updateStockOutLineStatus({
  2312. id: lot.stockOutLineId,
  2313. status: newStatus,
  2314. qty: cumulativeQty // Use cumulative quantity
  2315. });
  2316. if (submitQty > 0) {
  2317. await updateInventoryLotLineQuantities({
  2318. inventoryLotLineId: lot.lotId,
  2319. qty: submitQty,
  2320. status: 'available',
  2321. operation: 'pick'
  2322. });
  2323. }
  2324. // Check if pick order is completed when lot status becomes 'completed'
  2325. if (newStatus === 'completed' && lot.pickOrderConsoCode) {
  2326. console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
  2327. try {
  2328. const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
  2329. console.log(` Pick order completion check result:`, completionResponse);
  2330. if (completionResponse.code === "SUCCESS") {
  2331. console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
  2332. } else if (completionResponse.message === "not completed") {
  2333. console.log(`⏳ Pick order not completed yet, more lines remaining`);
  2334. } else {
  2335. console.error(` Error checking completion: ${completionResponse.message}`);
  2336. }
  2337. } catch (error) {
  2338. console.error("Error checking pick order completion:", error);
  2339. }
  2340. }
  2341. await fetchAllCombinedLotData();
  2342. console.log("Pick quantity submitted successfully!");
  2343. setTimeout(() => {
  2344. checkAndAutoAssignNext();
  2345. }, 1000);
  2346. } catch (error) {
  2347. console.error("Error submitting pick quantity:", error);
  2348. } finally {
  2349. if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: false }));
  2350. }
  2351. }, [fetchAllCombinedLotData, checkAndAutoAssignNext, actionBusyBySolId]);
  2352. const handleSkip = useCallback(async (lot: any) => {
  2353. try {
  2354. console.log("Skip clicked, submit lot required qty for lot:", lot.lotNo);
  2355. await handleSubmitPickQtyWithQty(lot, lot.requiredQty);
  2356. } catch (err) {
  2357. console.error("Error in Skip:", err);
  2358. }
  2359. }, [handleSubmitPickQtyWithQty]);
  2360. const handleStartScan = useCallback(() => {
  2361. const startTime = performance.now();
  2362. console.log(`⏱️ [START SCAN] Called at: ${new Date().toISOString()}`);
  2363. console.log(`⏱️ [START SCAN] Starting manual QR scan...`);
  2364. setIsManualScanning(true);
  2365. const setManualScanningTime = performance.now() - startTime;
  2366. console.log(`⏱️ [START SCAN] setManualScanning time: ${setManualScanningTime.toFixed(2)}ms`);
  2367. setProcessedQrCodes(new Set());
  2368. setLastProcessedQr('');
  2369. setQrScanError(false);
  2370. setQrScanSuccess(false);
  2371. const beforeStartScanTime = performance.now();
  2372. startScan();
  2373. const startScanTime = performance.now() - beforeStartScanTime;
  2374. console.log(`⏱️ [START SCAN] startScan() call time: ${startScanTime.toFixed(2)}ms`);
  2375. const totalTime = performance.now() - startTime;
  2376. console.log(`⏱️ [START SCAN] Total start scan time: ${totalTime.toFixed(2)}ms`);
  2377. console.log(`⏰ [START SCAN] Start scan completed at: ${new Date().toISOString()}`);
  2378. }, [startScan]);
  2379. const handlePickOrderSwitch = useCallback(async (pickOrderId: number) => {
  2380. if (pickOrderSwitching) return;
  2381. setPickOrderSwitching(true);
  2382. try {
  2383. console.log(" Switching to pick order:", pickOrderId);
  2384. setSelectedPickOrderId(pickOrderId);
  2385. // 强制刷新数据,确保显示正确的 pick order 数据
  2386. await fetchAllCombinedLotData(currentUserId, pickOrderId);
  2387. } catch (error) {
  2388. console.error("Error switching pick order:", error);
  2389. } finally {
  2390. setPickOrderSwitching(false);
  2391. }
  2392. }, [pickOrderSwitching, currentUserId, fetchAllCombinedLotData]);
  2393. const handleStopScan = useCallback(() => {
  2394. console.log("⏸️ Pausing QR scanner...");
  2395. setIsManualScanning(false);
  2396. setQrScanError(false);
  2397. setQrScanSuccess(false);
  2398. stopScan();
  2399. resetScan();
  2400. }, [stopScan, resetScan]);
  2401. // ... existing code around line 1469 ...
  2402. const handlelotnull = useCallback(async (lot: any) => {
  2403. // 优先使用 stockouts 中的 id,如果没有则使用 stockOutLineId
  2404. const stockOutLineId = lot.stockOutLineId;
  2405. if (!stockOutLineId) {
  2406. console.error(" No stockOutLineId found for lot:", lot);
  2407. return;
  2408. }
  2409. const solId = Number(stockOutLineId);
  2410. if (solId > 0 && actionBusyBySolId[solId]) {
  2411. console.warn("Action already in progress for stockOutLineId:", solId);
  2412. return;
  2413. }
  2414. try {
  2415. if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: true }));
  2416. // Step 1: Update stock out line status
  2417. await updateStockOutLineStatus({
  2418. id: stockOutLineId,
  2419. status: 'completed',
  2420. qty: 0
  2421. });
  2422. // Step 2: Create pick execution issue for no-lot case
  2423. // Get pick order ID from fgPickOrders or use 0 if not available
  2424. const pickOrderId = lot.pickOrderId || fgPickOrders[0]?.pickOrderId || 0;
  2425. const pickOrderCode = lot.pickOrderCode || fgPickOrders[0]?.pickOrderCode || lot.pickOrderConsoCode || '';
  2426. const issueData: PickExecutionIssueData = {
  2427. type: "Do", // Delivery Order type
  2428. pickOrderId: pickOrderId,
  2429. pickOrderCode: pickOrderCode,
  2430. pickOrderCreateDate: dayjs().format('YYYY-MM-DD'), // Use dayjs format
  2431. pickExecutionDate: dayjs().format('YYYY-MM-DD'),
  2432. pickOrderLineId: lot.pickOrderLineId,
  2433. itemId: lot.itemId,
  2434. itemCode: lot.itemCode || '',
  2435. itemDescription: lot.itemName || '',
  2436. lotId: null, // No lot available
  2437. lotNo: null, // No lot number
  2438. storeLocation: lot.location || '',
  2439. requiredQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0,
  2440. actualPickQty: 0, // No items picked (no lot available)
  2441. missQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0, // All quantity is missing
  2442. badItemQty: 0,
  2443. issueRemark: `No lot available for this item. Handled via handlelotnull.`,
  2444. pickerName: session?.user?.name || '',
  2445. };
  2446. const result = await recordPickExecutionIssue(issueData);
  2447. console.log(" Pick execution issue created for no-lot item:", result);
  2448. if (result && result.code === "SUCCESS") {
  2449. console.log(" No-lot item handled and issue recorded successfully");
  2450. } else {
  2451. console.error(" Failed to record pick execution issue:", result);
  2452. }
  2453. // Step 3: Refresh data
  2454. await fetchAllCombinedLotData();
  2455. } catch (error) {
  2456. console.error(" Error in handlelotnull:", error);
  2457. } finally {
  2458. if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: false }));
  2459. }
  2460. }, [fetchAllCombinedLotData, session, currentUserId, fgPickOrders, actionBusyBySolId]);
  2461. const handleBatchScan = useCallback(async () => {
  2462. const startTime = performance.now();
  2463. console.log(`⏱️ [BATCH SCAN START]`);
  2464. console.log(`⏰ Start time: ${new Date().toISOString()}`);
  2465. // 获取所有活跃批次(未扫描的)
  2466. const activeLots = combinedLotData.filter(lot => {
  2467. return (
  2468. lot.lotAvailability !== 'rejected' &&
  2469. lot.stockOutLineStatus !== 'rejected' &&
  2470. lot.stockOutLineStatus !== 'completed' &&
  2471. lot.stockOutLineStatus !== 'checked' && // ✅ 只处理未扫描的
  2472. lot.processingStatus !== 'completed' &&
  2473. lot.noLot !== true &&
  2474. lot.lotNo // ✅ 必须有 lotNo
  2475. );
  2476. });
  2477. if (activeLots.length === 0) {
  2478. console.log("No active lots to scan");
  2479. return;
  2480. }
  2481. console.log(`📦 Batch scanning ${activeLots.length} active lots using batch API...`);
  2482. try {
  2483. // ✅ 转换为批量扫描 API 所需的格式
  2484. const lines: BatchScanLineRequest[] = activeLots.map((lot) => ({
  2485. pickOrderLineId: Number(lot.pickOrderLineId),
  2486. inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null,
  2487. pickOrderConsoCode: String(lot.pickOrderConsoCode || ''),
  2488. lotNo: lot.lotNo || null,
  2489. itemId: Number(lot.itemId),
  2490. itemCode: String(lot.itemCode || ''),
  2491. stockOutLineId: lot.stockOutLineId ? Number(lot.stockOutLineId) : null, // ✅ 新增
  2492. }));
  2493. const request: BatchScanRequest = {
  2494. userId: currentUserId || 0,
  2495. lines: lines
  2496. };
  2497. console.log(`📤 Sending batch scan request with ${lines.length} lines`);
  2498. console.log(`📋 Request data:`, JSON.stringify(request, null, 2));
  2499. const scanStartTime = performance.now();
  2500. // ✅ 使用新的批量扫描 API(一次性处理所有请求)
  2501. const result = await batchScan(request);
  2502. const scanTime = performance.now() - scanStartTime;
  2503. console.log(`⏱️ Batch scan API call completed in ${scanTime.toFixed(2)}ms (${(scanTime / 1000).toFixed(3)}s)`);
  2504. console.log(`📥 Batch scan result:`, result);
  2505. // ✅ 刷新数据以获取最新的状态
  2506. const refreshStartTime = performance.now();
  2507. await fetchAllCombinedLotData();
  2508. const refreshTime = performance.now() - refreshStartTime;
  2509. console.log(`⏱️ Data refresh time: ${refreshTime.toFixed(2)}ms (${(refreshTime / 1000).toFixed(3)}s)`);
  2510. const totalTime = performance.now() - startTime;
  2511. console.log(`⏱️ [BATCH SCAN END]`);
  2512. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  2513. console.log(`⏰ End time: ${new Date().toISOString()}`);
  2514. if (result && result.code === "SUCCESS") {
  2515. setQrScanSuccess(true);
  2516. setQrScanError(false);
  2517. } else {
  2518. console.error("❌ Batch scan failed:", result);
  2519. setQrScanError(true);
  2520. setQrScanSuccess(false);
  2521. }
  2522. } catch (error) {
  2523. console.error("❌ Error in batch scan:", error);
  2524. setQrScanError(true);
  2525. setQrScanSuccess(false);
  2526. }
  2527. }, [combinedLotData, fetchAllCombinedLotData, currentUserId]);
  2528. const handleSubmitAllScanned = useCallback(async () => {
  2529. const startTime = performance.now();
  2530. console.log(`⏱️ [BATCH SUBMIT START]`);
  2531. console.log(`⏰ Start time: ${new Date().toISOString()}`);
  2532. const scannedLots = combinedLotData.filter(lot => {
  2533. const status = lot.stockOutLineStatus;
  2534. // ✅ noLot 情况:允许 checked / pending / partially_completed / PARTIALLY_COMPLETE
  2535. if (lot.noLot === true) {
  2536. return status === 'checked' ||
  2537. status === 'partially_completed' ||
  2538. status === 'PARTIALLY_COMPLETE';
  2539. }
  2540. // ✅ 正常 lot:也放宽为允许 checked / pending / partially_completed / PARTIALLY_COMPLETE
  2541. // 这样即使用户先改数(状态变为 pending / partially_completed),仍然可以批量提交
  2542. return status === 'checked' ||
  2543. status === 'partially_completed' ||
  2544. status === 'PARTIALLY_COMPLETE';
  2545. });
  2546. if (scannedLots.length === 0) {
  2547. console.log("No scanned items to submit");
  2548. return;
  2549. }
  2550. setIsSubmittingAll(true);
  2551. console.log(`📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`);
  2552. try {
  2553. // 转换为 batchSubmitList 所需的格式(与后端 QrPickBatchSubmitRequest 匹配)
  2554. const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => {
  2555. // 1. 需求数量
  2556. const requiredQty =
  2557. Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0);
  2558. // 2. 当前已经拣到的数量
  2559. // issue form 不会写回 SOL.qty,所以如果这条 SOL 有 issue,就用 issue form 的 actualPickQty 作为“已拣到数量”
  2560. const solId = Number(lot.stockOutLineId) || 0;
  2561. const issuePicked = solId > 0 ? issuePickedQtyBySolId[solId] : undefined;
  2562. const currentActualPickQty = Number(issuePicked ?? lot.actualPickQty ?? 0);
  2563. // 🔹 判断是否走“只改状态模式”
  2564. // 这里先给一个简单条件示例:如果你不想再补拣,只想把当前数量标记完成,
  2565. // 就让这个条件为 true(后面你可以根据业务加 UI 开关或别的 flag)。
  2566. const onlyComplete = lot.stockOutLineStatus === "partially_completed" || issuePicked !== undefined;
  2567. // lot.stockOutLineStatus === "partially_completed" && false === true;
  2568. let targetActual: number;
  2569. let newStatus: string;
  2570. if (onlyComplete) {
  2571. // ✅ 只改状态:目标数量 = 当前数量,不再补拣
  2572. targetActual = currentActualPickQty;
  2573. newStatus = "completed";
  2574. } else {
  2575. // ✅ 补拣模式:把剩余全部拣完
  2576. const remainingQty = Math.max(0, requiredQty - currentActualPickQty);
  2577. const cumulativeQty = currentActualPickQty + remainingQty;
  2578. targetActual = cumulativeQty;
  2579. newStatus = "partially_completed";
  2580. if (requiredQty > 0 && cumulativeQty >= requiredQty) {
  2581. newStatus = "completed";
  2582. }
  2583. }
  2584. return {
  2585. stockOutLineId: Number(lot.stockOutLineId) || 0,
  2586. pickOrderLineId: Number(lot.pickOrderLineId),
  2587. inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null,
  2588. requiredQty,
  2589. // 后端用 targetActual - 当前 qty 算增量,onlyComplete 时就是 0
  2590. actualPickQty: targetActual,
  2591. stockOutLineStatus: newStatus,
  2592. pickOrderConsoCode: String(lot.pickOrderConsoCode || ""),
  2593. noLot: Boolean(lot.noLot === true),
  2594. };
  2595. });
  2596. const request: batchSubmitListRequest = {
  2597. userId: currentUserId || 0,
  2598. lines: lines
  2599. };
  2600. console.log(`📤 Sending batch submit request with ${lines.length} lines`);
  2601. console.log(`📋 Request data:`, JSON.stringify(request, null, 2));
  2602. const submitStartTime = performance.now();
  2603. // 使用 batchSubmitList API
  2604. const result = await batchSubmitList(request);
  2605. const submitTime = performance.now() - submitStartTime;
  2606. console.log(`⏱️ Batch submit API call completed in ${submitTime.toFixed(2)}ms (${(submitTime / 1000).toFixed(3)}s)`);
  2607. console.log(`📥 Batch submit result:`, result);
  2608. // Refresh data once after batch submission
  2609. const refreshStartTime = performance.now();
  2610. await fetchAllCombinedLotData();
  2611. const refreshTime = performance.now() - refreshStartTime;
  2612. console.log(`⏱️ Data refresh time: ${refreshTime.toFixed(2)}ms (${(refreshTime / 1000).toFixed(3)}s)`);
  2613. const totalTime = performance.now() - startTime;
  2614. console.log(`⏱️ [BATCH SUBMIT END]`);
  2615. console.log(`⏱️ Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`);
  2616. console.log(`⏰ End time: ${new Date().toISOString()}`);
  2617. if (result && result.code === "SUCCESS") {
  2618. setQrScanSuccess(true);
  2619. setTimeout(() => {
  2620. setQrScanSuccess(false);
  2621. checkAndAutoAssignNext();
  2622. if (onSwitchToRecordTab) {
  2623. onSwitchToRecordTab();
  2624. }
  2625. if (onRefreshReleasedOrderCount) {
  2626. onRefreshReleasedOrderCount();
  2627. }
  2628. }, 2000);
  2629. } else {
  2630. console.error("Batch submit failed:", result);
  2631. setQrScanError(true);
  2632. }
  2633. } catch (error) {
  2634. console.error("Error submitting all scanned items:", error);
  2635. setQrScanError(true);
  2636. } finally {
  2637. setIsSubmittingAll(false);
  2638. }
  2639. }, [combinedLotData, fetchAllCombinedLotData, checkAndAutoAssignNext, currentUserId, onSwitchToRecordTab, onRefreshReleasedOrderCount, issuePickedQtyBySolId]);
  2640. // Calculate scanned items count
  2641. // Calculate scanned items count (should match handleSubmitAllScanned filter logic)
  2642. const scannedItemsCount = useMemo(() => {
  2643. const filtered = combinedLotData.filter(lot => {
  2644. const status = lot.stockOutLineStatus;
  2645. // ✅ 与 handleSubmitAllScanned 完全保持一致
  2646. if (lot.noLot === true) {
  2647. return status === 'checked' ||
  2648. status === 'partially_completed' ||
  2649. status === 'PARTIALLY_COMPLETE';
  2650. }
  2651. return status === 'checked' ||
  2652. status === 'partially_completed' ||
  2653. status === 'PARTIALLY_COMPLETE';
  2654. });
  2655. // 添加调试日志
  2656. const noLotCount = filtered.filter(l => l.noLot === true).length;
  2657. const normalCount = filtered.filter(l => l.noLot !== true).length;
  2658. console.log(`📊 scannedItemsCount calculation: total=${filtered.length}, noLot=${noLotCount}, normal=${normalCount}`);
  2659. console.log(`📊 All items breakdown:`, {
  2660. total: combinedLotData.length,
  2661. noLot: combinedLotData.filter(l => l.noLot === true).length,
  2662. normal: combinedLotData.filter(l => l.noLot !== true).length
  2663. });
  2664. return filtered.length;
  2665. }, [combinedLotData]);
  2666. /*
  2667. // ADD THIS: Auto-stop scan when no data available
  2668. useEffect(() => {
  2669. if (isManualScanning && combinedLotData.length === 0) {
  2670. console.log("⏹️ No data available, auto-stopping QR scan...");
  2671. handleStopScan();
  2672. }
  2673. }, [combinedLotData.length, isManualScanning, handleStopScan]);
  2674. */
  2675. // Cleanup effect
  2676. useEffect(() => {
  2677. return () => {
  2678. // Cleanup when component unmounts (e.g., when switching tabs)
  2679. if (isManualScanning) {
  2680. console.log("🧹 Pick execution component unmounting, stopping QR scanner...");
  2681. stopScan();
  2682. resetScan();
  2683. }
  2684. };
  2685. }, [isManualScanning, stopScan, resetScan]);
  2686. const getStatusMessage = useCallback((lot: any) => {
  2687. switch (lot.stockOutLineStatus?.toLowerCase()) {
  2688. case 'pending':
  2689. return t("Please finish QR code scan and pick order.");
  2690. case 'checked':
  2691. return t("Please submit the pick order.");
  2692. case 'partially_completed':
  2693. return t("Partial quantity submitted. Please submit more or complete the order.");
  2694. case 'completed':
  2695. return t("Pick order completed successfully!");
  2696. case 'rejected':
  2697. return t("Lot has been rejected and marked as unavailable.");
  2698. case 'unavailable':
  2699. return t("This order is insufficient, please pick another lot.");
  2700. default:
  2701. return t("Please finish QR code scan and pick order.");
  2702. }
  2703. }, [t]);
  2704. return (
  2705. <TestQrCodeProvider
  2706. lotData={combinedLotData}
  2707. onScanLot={handleQrCodeSubmit}
  2708. onBatchScan={handleBatchScan}
  2709. filterActive={(lot) => (
  2710. lot.lotAvailability !== 'rejected' &&
  2711. lot.stockOutLineStatus !== 'rejected' &&
  2712. lot.stockOutLineStatus !== 'completed'
  2713. )}
  2714. >
  2715. <FormProvider {...formProps}>
  2716. <Stack spacing={2}>
  2717. <Box
  2718. sx={{
  2719. position: 'fixed',
  2720. top: 0,
  2721. left: 0,
  2722. right: 0,
  2723. zIndex: 1100, // Higher than other elements
  2724. backgroundColor: 'background.paper',
  2725. pt: 2,
  2726. pb: 1,
  2727. px: 2,
  2728. borderBottom: '1px solid',
  2729. borderColor: 'divider',
  2730. boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
  2731. }}
  2732. >
  2733. <LinearProgressWithLabel
  2734. completed={progress.completed}
  2735. total={progress.total}
  2736. label={t("Progress")}
  2737. />
  2738. <ScanStatusAlert
  2739. error={qrScanError}
  2740. success={qrScanSuccess}
  2741. errorMessage={t("QR code does not match any item in current orders.")}
  2742. successMessage={t("QR code verified.")}
  2743. />
  2744. </Box>
  2745. {/* DO Header */}
  2746. {/* 保留:Combined Lot Table - 包含所有 QR 扫描功能 */}
  2747. <Box>
  2748. <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2, mt: 10 }}>
  2749. <Typography variant="h6" gutterBottom sx={{ mb: 0 }}>
  2750. {t("All Pick Order Lots")}
  2751. </Typography>
  2752. <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
  2753. {/* Scanner status indicator (always visible) */}
  2754. {/*
  2755. <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
  2756. <QrCodeIcon
  2757. sx={{
  2758. color: isManualScanning ? '#4caf50' : '#9e9e9e',
  2759. animation: isManualScanning ? 'pulse 2s infinite' : 'none',
  2760. '@keyframes pulse': {
  2761. '0%, 100%': { opacity: 1 },
  2762. '50%': { opacity: 0.5 }
  2763. }
  2764. }}
  2765. />
  2766. <Typography variant="body2" sx={{ color: isManualScanning ? '#4caf50' : '#9e9e9e' }}>
  2767. {isManualScanning ? t("Scanner Active") : t("Scanner Inactive")}
  2768. </Typography>
  2769. </Box>
  2770. */}
  2771. {/* Pause/Resume button instead of Start/Stop */}
  2772. {isManualScanning ? (
  2773. <Button
  2774. variant="outlined"
  2775. startIcon={<QrCodeIcon />}
  2776. onClick={handleStopScan}
  2777. color="secondary"
  2778. sx={{ minWidth: '120px' }}
  2779. >
  2780. {t("Stop QR Scan")}
  2781. </Button>
  2782. ) : (
  2783. <Button
  2784. variant="contained"
  2785. startIcon={<QrCodeIcon />}
  2786. onClick={handleStartScan}
  2787. color="primary"
  2788. sx={{ minWidth: '120px' }}
  2789. >
  2790. {t("Start QR Scan")}
  2791. </Button>
  2792. )}
  2793. {/* 保留:Submit All Scanned Button */}
  2794. <Button
  2795. variant="contained"
  2796. color="success"
  2797. onClick={handleSubmitAllScanned}
  2798. disabled={
  2799. scannedItemsCount === 0
  2800. || isSubmittingAll}
  2801. sx={{ minWidth: '160px' }}
  2802. >
  2803. {isSubmittingAll ? (
  2804. <>
  2805. <CircularProgress size={16} sx={{ mr: 1, color: 'white' }} />
  2806. {t("Submitting...")}
  2807. </>
  2808. ) : (
  2809. `${t("Submit All Scanned")} (${scannedItemsCount})`
  2810. )}
  2811. </Button>
  2812. </Box>
  2813. </Box>
  2814. {fgPickOrders.length > 0 && (
  2815. <Paper sx={{ p: 2, mb: 2 }}>
  2816. <Stack spacing={2}>
  2817. {/* 基本信息 */}
  2818. <Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
  2819. <Typography variant="subtitle1">
  2820. <strong>{t("Shop Name")}:</strong> {fgPickOrders[0].shopName || '-'}
  2821. </Typography>
  2822. <Typography variant="subtitle1">
  2823. <strong>{t("Store ID")}:</strong> {fgPickOrders[0].storeId || '-'}
  2824. </Typography>
  2825. <Typography variant="subtitle1">
  2826. <strong>{t("Ticket No.")}:</strong> {fgPickOrders[0].ticketNo || '-'}
  2827. </Typography>
  2828. <Typography variant="subtitle1">
  2829. <strong>{t("Departure Time")}:</strong> {fgPickOrders[0].DepartureTime || '-'}
  2830. </Typography>
  2831. </Stack>
  2832. {/* 改进:三个字段显示在一起,使用表格式布局 */}
  2833. {/* 改进:三个字段合并显示 */}
  2834. {/* 改进:表格式显示每个 pick order */}
  2835. <Box sx={{
  2836. p: 2,
  2837. backgroundColor: '#f5f5f5',
  2838. borderRadius: 1
  2839. }}>
  2840. <Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
  2841. {t("Pick Orders Details")}:
  2842. </Typography>
  2843. {(() => {
  2844. const pickOrderCodes = fgPickOrders[0].pickOrderCodes as string[] | string | undefined;
  2845. const deliveryNos = fgPickOrders[0].deliveryNos as string[] | string | undefined;
  2846. const lineCounts = fgPickOrders[0].lineCountsPerPickOrder;
  2847. const pickOrderCodesArray = Array.isArray(pickOrderCodes)
  2848. ? pickOrderCodes
  2849. : (typeof pickOrderCodes === 'string' ? pickOrderCodes.split(', ') : []);
  2850. const deliveryNosArray = Array.isArray(deliveryNos)
  2851. ? deliveryNos
  2852. : (typeof deliveryNos === 'string' ? deliveryNos.split(', ') : []);
  2853. const lineCountsArray = Array.isArray(lineCounts) ? lineCounts : [];
  2854. const maxLength = Math.max(
  2855. pickOrderCodesArray.length,
  2856. deliveryNosArray.length,
  2857. lineCountsArray.length
  2858. );
  2859. if (maxLength === 0) {
  2860. return <Typography variant="body2" color="text.secondary">-</Typography>;
  2861. }
  2862. // 使用与外部基本信息相同的样式
  2863. return Array.from({ length: maxLength }, (_, idx) => (
  2864. <Stack
  2865. key={idx}
  2866. direction="row"
  2867. spacing={4}
  2868. useFlexGap
  2869. flexWrap="wrap"
  2870. sx={{ mb: idx < maxLength - 1 ? 1 : 0 }} // 除了最后一行,都添加底部间距
  2871. >
  2872. <Typography variant="subtitle1">
  2873. <strong>{t("Delivery Order")}:</strong> {deliveryNosArray[idx] || '-'}
  2874. </Typography>
  2875. <Typography variant="subtitle1">
  2876. <strong>{t("Pick Order")}:</strong> {pickOrderCodesArray[idx] || '-'}
  2877. </Typography>
  2878. <Typography variant="subtitle1">
  2879. <strong>{t("Finsihed good items")}:</strong> {lineCountsArray[idx] || '-'}<strong>{t("kinds")}</strong>
  2880. </Typography>
  2881. </Stack>
  2882. ));
  2883. })()}
  2884. </Box>
  2885. </Stack>
  2886. </Paper>
  2887. )}
  2888. <TableContainer component={Paper}>
  2889. <Table>
  2890. <TableHead>
  2891. <TableRow>
  2892. <TableCell>{t("Index")}</TableCell>
  2893. <TableCell>{t("Route")}</TableCell>
  2894. <TableCell>{t("Item Code")}</TableCell>
  2895. <TableCell>{t("Item Name")}</TableCell>
  2896. <TableCell>{t("Lot#")}</TableCell>
  2897. <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
  2898. <TableCell align="center">{t("Scan Result")}</TableCell>
  2899. <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
  2900. </TableRow>
  2901. </TableHead>
  2902. <TableBody>
  2903. {paginatedData.length === 0 ? (
  2904. <TableRow>
  2905. <TableCell colSpan={11} align="center">
  2906. <Typography variant="body2" color="text.secondary">
  2907. {t("No data available")}
  2908. </Typography>
  2909. </TableCell>
  2910. </TableRow>
  2911. ) : (
  2912. // 在第 1797-1938 行之间,将整个 map 函数修改为:
  2913. paginatedData.map((lot, index) => {
  2914. // 检查是否是 issue lot
  2915. const isIssueLot = lot.stockOutLineStatus === 'rejected' || !lot.lotNo;
  2916. return (
  2917. <TableRow
  2918. key={`${lot.pickOrderLineId}-${lot.lotId || 'null'}`}
  2919. sx={{
  2920. //backgroundColor: isIssueLot ? '#fff3e0' : 'inherit',
  2921. // opacity: isIssueLot ? 0.6 : 1,
  2922. '& .MuiTableCell-root': {
  2923. //color: isIssueLot ? 'warning.main' : 'inherit'
  2924. }
  2925. }}
  2926. >
  2927. <TableCell>
  2928. <Typography variant="body2" fontWeight="bold">
  2929. {paginationController.pageNum * paginationController.pageSize + index + 1}
  2930. </Typography>
  2931. </TableCell>
  2932. <TableCell>
  2933. <Typography variant="body2">
  2934. {lot.routerRoute || '-'}
  2935. </Typography>
  2936. </TableCell>
  2937. <TableCell>{lot.itemCode}</TableCell>
  2938. <TableCell>{lot.itemName + '(' + lot.stockUnit + ')'}</TableCell>
  2939. <TableCell>
  2940. <Box>
  2941. <Typography
  2942. sx={{
  2943. // color: isIssueLot ? 'warning.main' : lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit',
  2944. }}
  2945. >
  2946. {lot.lotNo ||
  2947. t('Please check around have QR code or not, may be have just now stock in or transfer in or transfer out.')}
  2948. </Typography>
  2949. </Box>
  2950. </TableCell>
  2951. <TableCell align="right">
  2952. {(() => {
  2953. const requiredQty = lot.requiredQty || 0;
  2954. return requiredQty.toLocaleString() + '(' + lot.uomShortDesc + ')';
  2955. })()}
  2956. </TableCell>
  2957. <TableCell align="center">
  2958. {(() => {
  2959. const status = lot.stockOutLineStatus?.toLowerCase();
  2960. const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
  2961. const isNoLot = !lot.lotNo;
  2962. // rejected lot:显示红色勾选(已扫描但被拒绝)
  2963. if (isRejected && !isNoLot) {
  2964. return (
  2965. <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  2966. <Checkbox
  2967. checked={true}
  2968. disabled={true}
  2969. readOnly={true}
  2970. size="large"
  2971. sx={{
  2972. color: 'error.main',
  2973. '&.Mui-checked': { color: 'error.main' },
  2974. transform: 'scale(1.3)',
  2975. }}
  2976. />
  2977. </Box>
  2978. );
  2979. }
  2980. // 正常 lot:已扫描(checked/partially_completed/completed)
  2981. if (!isNoLot && status !== 'pending' && status !== 'rejected') {
  2982. return (
  2983. <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  2984. <Checkbox
  2985. checked={true}
  2986. disabled={true}
  2987. readOnly={true}
  2988. size="large"
  2989. sx={{
  2990. color: 'success.main',
  2991. '&.Mui-checked': { color: 'success.main' },
  2992. transform: 'scale(1.3)',
  2993. }}
  2994. />
  2995. </Box>
  2996. );
  2997. }
  2998. // noLot 且已完成/部分完成:显示红色勾选
  2999. if (isNoLot && (status === 'partially_completed' || status === 'completed')) {
  3000. return (
  3001. <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
  3002. <Checkbox
  3003. checked={true}
  3004. disabled={true}
  3005. readOnly={true}
  3006. size="large"
  3007. sx={{
  3008. color: 'error.main',
  3009. '&.Mui-checked': { color: 'error.main' },
  3010. transform: 'scale(1.3)',
  3011. }}
  3012. />
  3013. </Box>
  3014. );
  3015. }
  3016. return null;
  3017. })()}
  3018. </TableCell>
  3019. <TableCell align="center">
  3020. <Box sx={{ display: 'flex', justifyContent: 'center' }}>
  3021. {(() => {
  3022. const status = lot.stockOutLineStatus?.toLowerCase();
  3023. const isRejected = status === 'rejected' || lot.lotAvailability === 'rejected';
  3024. const isNoLot = !lot.lotNo;
  3025. // ✅ rejected lot:显示提示文本(换行显示)
  3026. if (isRejected && !isNoLot) {
  3027. return (
  3028. <Typography
  3029. variant="body2"
  3030. color="error.main"
  3031. sx={{
  3032. textAlign: 'center',
  3033. whiteSpace: 'normal',
  3034. wordBreak: 'break-word',
  3035. maxWidth: '200px',
  3036. lineHeight: 1.5
  3037. }}
  3038. >
  3039. {t("This lot is rejected, please scan another lot.")}
  3040. </Typography>
  3041. );
  3042. }
  3043. // noLot 情况:只显示 Issue 按钮
  3044. if (isNoLot) {
  3045. return (
  3046. <Button
  3047. variant="outlined"
  3048. size="small"
  3049. onClick={() => handlelotnull(lot)}
  3050. disabled={
  3051. status === 'completed' ||
  3052. (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true)
  3053. }
  3054. sx={{
  3055. fontSize: '0.7rem',
  3056. py: 0.5,
  3057. minHeight: '28px',
  3058. minWidth: '60px',
  3059. borderColor: 'warning.main',
  3060. color: 'warning.main'
  3061. }}
  3062. >
  3063. {t("Issue")}
  3064. </Button>
  3065. );
  3066. }
  3067. // 正常 lot:显示 Submit 和 Issue 按钮
  3068. return (
  3069. <Stack direction="row" spacing={1} alignItems="center">
  3070. <Button
  3071. variant="contained"
  3072. onClick={() => {
  3073. const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
  3074. const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
  3075. handlePickQtyChange(lotKey, submitQty);
  3076. handleSubmitPickQtyWithQty(lot, submitQty);
  3077. }}
  3078. disabled={
  3079. lot.lotAvailability === 'expired' ||
  3080. lot.lotAvailability === 'status_unavailable' ||
  3081. lot.lotAvailability === 'rejected' ||
  3082. lot.stockOutLineStatus === 'completed' ||
  3083. lot.stockOutLineStatus === 'pending' ||
  3084. (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true)
  3085. }
  3086. sx={{ fontSize: '0.75rem', py: 0.5, minHeight: '28px', minWidth: '70px' }}
  3087. >
  3088. {t("Submit")}
  3089. </Button>
  3090. <Button
  3091. variant="outlined"
  3092. size="small"
  3093. onClick={() => handlePickExecutionForm(lot)}
  3094. disabled={
  3095. lot.lotAvailability === 'expired' ||
  3096. lot.stockOutLineStatus === 'completed' ||
  3097. (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true)
  3098. }
  3099. sx={{
  3100. fontSize: '0.7rem',
  3101. py: 0.5,
  3102. minHeight: '28px',
  3103. minWidth: '60px',
  3104. borderColor: 'warning.main',
  3105. color: 'warning.main'
  3106. }}
  3107. title="Report missing or bad items"
  3108. >
  3109. {t("Edit")}
  3110. </Button>
  3111. <Button
  3112. variant="outlined"
  3113. size="small"
  3114. onClick={() => handleSkip(lot)}
  3115. disabled={
  3116. lot.stockOutLineStatus === 'completed' ||
  3117. // 使用 issue form 後,禁用「Just Completed」(避免再次点击造成重复提交)
  3118. (Number(lot.stockOutLineId) > 0 && issuePickedQtyBySolId[Number(lot.stockOutLineId)] !== undefined) ||
  3119. (Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true)
  3120. }
  3121. sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '60px' }}
  3122. >
  3123. {t("Just Completed")}
  3124. </Button>
  3125. </Stack>
  3126. );
  3127. })()}
  3128. </Box>
  3129. </TableCell>
  3130. </TableRow>
  3131. );
  3132. })
  3133. )}
  3134. </TableBody>
  3135. </Table>
  3136. </TableContainer>
  3137. <TablePagination
  3138. component="div"
  3139. count={combinedLotData.length}
  3140. page={paginationController.pageNum}
  3141. rowsPerPage={paginationController.pageSize}
  3142. onPageChange={handlePageChange}
  3143. onRowsPerPageChange={handlePageSizeChange}
  3144. rowsPerPageOptions={[10, 25, 50,-1]}
  3145. labelRowsPerPage={t("Rows per page")}
  3146. labelDisplayedRows={({ from, to, count }) =>
  3147. `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
  3148. }
  3149. />
  3150. </Box>
  3151. </Stack>
  3152. {/* QR Code Scanner works in background - no modal needed */}
  3153. <ManualLotConfirmationModal
  3154. open={manualLotConfirmationOpen}
  3155. onClose={() => {
  3156. setManualLotConfirmationOpen(false);
  3157. }}
  3158. onConfirm={handleManualLotConfirmation}
  3159. expectedLot={expectedLotData}
  3160. scannedLot={scannedLotData}
  3161. isLoading={isConfirmingLot}
  3162. />
  3163. {/* 保留:Lot Confirmation Modal */}
  3164. {lotConfirmationOpen && expectedLotData && scannedLotData && (
  3165. <LotConfirmationModal
  3166. open={lotConfirmationOpen}
  3167. onClose={() => {
  3168. console.log(`⏱️ [LOT CONFIRM MODAL] Closing modal, clearing state`);
  3169. clearLotConfirmationState(true);
  3170. }}
  3171. onConfirm={handleLotConfirmation}
  3172. expectedLot={expectedLotData}
  3173. scannedLot={scannedLotData}
  3174. isLoading={isConfirmingLot}
  3175. />
  3176. )}
  3177. {/* 保留:Good Pick Execution Form Modal */}
  3178. {pickExecutionFormOpen && selectedLotForExecutionForm && (
  3179. <GoodPickExecutionForm
  3180. open={pickExecutionFormOpen}
  3181. onClose={() => {
  3182. setPickExecutionFormOpen(false);
  3183. setSelectedLotForExecutionForm(null);
  3184. }}
  3185. onSubmit={handlePickExecutionFormSubmit}
  3186. selectedLot={selectedLotForExecutionForm}
  3187. selectedPickOrderLine={{
  3188. id: selectedLotForExecutionForm.pickOrderLineId,
  3189. itemId: selectedLotForExecutionForm.itemId,
  3190. itemCode: selectedLotForExecutionForm.itemCode,
  3191. itemName: selectedLotForExecutionForm.itemName,
  3192. pickOrderCode: selectedLotForExecutionForm.pickOrderCode,
  3193. availableQty: selectedLotForExecutionForm.availableQty || 0,
  3194. requiredQty: selectedLotForExecutionForm.requiredQty || 0,
  3195. // uomCode: selectedLotForExecutionForm.uomCode || '',
  3196. uomDesc: selectedLotForExecutionForm.uomDesc || '',
  3197. pickedQty: selectedLotForExecutionForm.actualPickQty || 0,
  3198. uomShortDesc: selectedLotForExecutionForm.uomShortDesc || '',
  3199. suggestedList: [],
  3200. noLotLines: [],
  3201. }}
  3202. pickOrderId={selectedLotForExecutionForm.pickOrderId}
  3203. pickOrderCreateDate={new Date()}
  3204. />
  3205. )}
  3206. </FormProvider>
  3207. </TestQrCodeProvider>
  3208. );
  3209. };
  3210. export default PickExecution;