FPSMS-frontend
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

GoodPickExecutiondetail.tsx 149 KiB

před 6 měsíci
před 6 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 5 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 5 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 4 měsíci
před 5 měsíci
před 5 měsíci
před 4 měsíci
před 3 měsíci
před 2 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 1 týdnem
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 2 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 6 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 6 měsíci
před 2 měsíci
před 1 týdnem
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 2 týdny
před 6 měsíci
před 4 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 3 měsíci
před 6 měsíci
před 3 měsíci
před 1 dnem
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 2 týdny
před 1 týdnem
před 2 týdny
před 6 měsíci
před 6 měsíci
před 3 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 6 měsíci
před 1 dnem
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 6 měsíci
před 5 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 2 týdny
před 2 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 2 měsíci
před 2 měsíci
před 2 měsíci
před 2 měsíci
před 2 měsíci
před 6 měsíci
před 2 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 6 měsíci
před 1 týdnem
před 2 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 2 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 2 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 2 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 2 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 2 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 1 měsícem
před 5 měsíci
před 1 měsícem
před 3 dny
před 1 měsícem
před 3 dny
před 1 měsícem
před 3 dny
před 1 měsícem
před 5 měsíci
před 1 měsícem
před 3 dny
před 1 měsícem
před 5 měsíci
před 1 měsícem
před 1 měsícem
před 5 měsíci
před 4 měsíci
před 5 měsíci
před 5 měsíci
před 4 měsíci
před 5 měsíci
před 3 měsíci
před 2 měsíci
před 3 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 6 měsíci
před 2 týdny
před 1 dnem
před 2 týdny
před 1 týdnem
před 2 týdny
před 6 měsíci
před 1 dnem
před 6 měsíci
před 3 měsíci
před 6 měsíci
před 2 měsíci
před 1 dnem
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 1 dnem
před 6 měsíci
před 6 měsíci
před 3 měsíci
před 6 měsíci
před 3 měsíci
před 2 týdny
před 3 měsíci
před 6 měsíci
před 3 měsíci
před 6 měsíci
před 1 dnem
před 6 měsíci
před 6 měsíci
před 1 dnem
před 2 týdny
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 5 měsíci
před 4 měsíci
před 5 měsíci
před 5 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 6 měsíci
před 4 měsíci
před 6 měsíci
před 6 měsíci
před 4 měsíci
před 6 měsíci
před 1 týdnem
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 6 měsíci
před 4 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 6 měsíci
před 4 měsíci
před 6 měsíci
před 4 měsíci
před 5 měsíci
před 6 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 6 měsíci
před 4 měsíci
před 1 týdnem
před 4 měsíci
před 1 týdnem
před 4 měsíci
před 1 týdnem
před 4 měsíci
před 1 týdnem
před 4 měsíci
před 1 týdnem
před 4 měsíci
před 1 týdnem
před 4 měsíci
před 1 týdnem
před 4 měsíci
před 1 týdnem
před 4 měsíci
před 2 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 2 měsíci
před 2 měsíci
před 2 měsíci
před 2 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 2 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 2 měsíci
před 1 týdnem
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 2 měsíci
před 1 týdnem
před 6 měsíci
před 2 měsíci
před 1 týdnem
před 6 měsíci
před 2 měsíci
před 1 týdnem
před 6 měsíci
před 2 měsíci
před 2 měsíci
před 1 týdnem
před 6 měsíci
před 1 dnem
před 1 týdnem
před 6 měsíci
před 1 dnem
před 1 týdnem
před 1 týdnem
před 6 měsíci
před 2 měsíci
před 2 měsíci
před 2 měsíci
před 2 měsíci
před 1 týdnem
před 1 týdnem
před 1 týdnem
před 4 měsíci
před 2 měsíci
před 4 měsíci
před 2 měsíci
před 1 týdnem
před 4 měsíci
před 4 měsíci
před 2 měsíci
před 1 týdnem
před 4 měsíci
před 2 měsíci
před 4 měsíci
před 2 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 4 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 4 měsíci
před 2 měsíci
před 4 měsíci
před 2 měsíci
před 4 měsíci
před 2 měsíci
před 4 měsíci
před 6 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 2 měsíci
před 6 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 6 měsíci
před 6 měsíci
před 1 týdnem
před 6 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 6 měsíci
před 2 měsíci
před 2 měsíci
před 6 měsíci
před 2 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 týdny
před 1 týdnem
před 2 týdny
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 2 týdny
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 2 týdny
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 3 měsíci
před 2 měsíci
před 1 týdnem
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 4 měsíci
před 6 měsíci
před 4 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 2 týdny
před 2 týdny
před 6 měsíci
před 4 měsíci
před 6 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 5 měsíci
před 2 měsíci
před 4 měsíci
před 3 měsíci
před 4 měsíci
před 3 měsíci
před 3 měsíci
před 4 měsíci
před 6 měsíci
před 2 týdny
před 6 měsíci
před 2 týdny
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 3 dny
před 6 měsíci
před 3 dny
před 6 měsíci
před 1 týdnem
před 3 dny
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 4 měsíci
před 6 měsíci
před 1 týdnem
před 6 měsíci
před 2 týdny
před 6 měsíci
před 1 týdnem
před 6 měsíci
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 5 měsíci
před 2 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 6 měsíci
před 2 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 4 měsíci
před 5 měsíci
před 2 týdny
před 5 měsíci
před 5 měsíci
před 2 týdny
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 4 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 4 měsíci
před 2 týdny
před 5 měsíci
před 2 týdny
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 1 týdnem
před 2 měsíci
před 3 měsíci
před 1 týdnem
před 3 měsíci
před 1 týdnem
před 3 měsíci
před 2 týdny
před 2 týdny
před 2 týdny
před 2 týdny
před 2 týdny
před 2 týdny
před 2 týdny
před 2 týdny
před 2 týdny
před 2 týdny
před 2 týdny
před 5 měsíci
před 3 měsíci
před 2 týdny
před 2 týdny
před 3 měsíci
před 2 týdny
před 3 měsíci
před 5 měsíci
před 6 měsíci
před 3 měsíci
před 6 měsíci
před 3 měsíci
před 6 měsíci
před 3 měsíci
před 4 měsíci
před 3 měsíci
před 1 týdnem
před 3 měsíci
před 4 měsíci
před 3 měsíci
před 4 měsíci
před 1 týdnem
před 4 měsíci
před 1 týdnem
před 3 měsíci
před 6 měsíci
před 3 měsíci
před 2 týdny
před 6 měsíci
před 5 měsíci
před 5 měsíci
před 1 týdnem
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 2 týdny
před 5 měsíci
před 6 měsíci
před 2 týdny
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 6 měsíci
před 6 měsíci
před 5 měsíci
před 5 měsíci
před 2 měsíci
před 5 měsíci
před 2 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 2 měsíci
před 5 měsíci
před 2 měsíci
před 5 měsíci
před 2 měsíci
před 5 měsíci
před 2 měsíci
před 5 měsíci
před 2 měsíci
před 5 měsíci
před 2 měsíci
před 5 měsíci
před 2 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 4 měsíci
před 4 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 4 měsíci
před 5 měsíci
před 1 týdnem
před 5 měsíci
před 4 měsíci
před 5 měsíci
před 4 měsíci
před 4 měsíci
před 5 měsíci
před 4 měsíci
před 4 měsíci
před 2 měsíci
před 4 měsíci
před 2 měsíci
před 4 měsíci
před 1 dnem
před 2 týdny
před 1 dnem
před 4 měsíci
před 4 měsíci
před 4 měsíci
před 1 dnem
před 4 měsíci
před 1 dnem
před 4 měsíci
před 2 týdny
před 4 měsíci
před 1 dnem
před 4 měsíci
před 1 dnem
před 4 měsíci
před 2 týdny
před 4 měsíci
před 1 dnem
před 4 měsíci
před 2 měsíci
před 4 měsíci
před 2 měsíci
před 1 dnem
před 2 týdny
před 1 týdnem
před 1 týdnem
před 1 týdnem
před 2 týdny
před 1 dnem
před 2 měsíci
před 2 měsíci
před 2 měsíci
před 4 měsíci
před 5 měsíci
před 2 měsíci
před 5 měsíci
před 2 měsíci
před 3 měsíci
před 5 měsíci
před 5 měsíci
před 1 týdnem
před 2 týdny
před 1 dnem
před 5 měsíci
před 5 měsíci
před 4 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 5 měsíci
před 6 měsíci
před 5 měsíci
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;