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

GoodPickExecutiondetail.tsx 153 KiB

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