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

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